epub, le convertisseur EPUB3 à la volée de LinuxFr.org

Posté par  (site web personnel) . Édité par Florent Zara et Julien Jorge. Modéré par Julien Jorge. Licence CC By‑SA.
28
4
nov.
2024
LinuxFr.org

Le site LinuxFr.org utilise divers logiciels libres pour son fonctionnement et ses services : une large majorité provient de projets tiers (Debian, MariaDB, Redis - version d’avant le changement de licence, nginx, Postfix, conteneurs LXC et Docker, Ruby On Rails, Sympa, etc.) et d’autres composants sont développés pour nos propres besoins. Cette dernière catégorie comprend le code principal du site web en Ruby On Rails, et principalement 5 services autour : le cache d’images img, la tribune board, le convertisseur EPUB 3 epub, le partageur sur les réseaux sociaux share et le convertisseur LaTeX vers SVG svg. Cette dépêche va s’intéresser à epub, un code sous AGPLv3.

Elle est née d’une envie personnelle d’expliquer, documenter et montrer ce qui a été fait sur le convertisseur EPUB3 à la volée de LinuxFr.org, et elle vient accompagner la précédente sur img, le cache d’images sur LinuxFr.org.

Sommaire

Des EPUB de vos contenus et commentaires

LinuxFr.org vous permet de lire les contenus et commentaires du site, au format EPUB3, par exemple dans votre liseuse préférée. Il y a une exception à cela, les liens, parce que certes ça ferait des EPUB tout mignons, mais surtout petits voire un poil inutiles. Le lien EPUB est présent automatiquement sur chaque contenu (hormis les liens donc).

Le principe est simple : on donne un lien vers un contenu HTML à epub, il le demande à la partie Ruby on Rails du site, ainsi que les images associées, convertit le tout au format EPUB3 et le renvoie à la personne qui l’a demandé. Techniquement epub n'est pas exposé frontalement mais se trouve derrière un nginx.

Côté code Ruby on Rails

C’est assez basique : on ajoute juste sur chaque contenu un lien pour télécharger au format EPUB. Ainsi, y compris sur cette dépêche, vous allez trouver un lien à la fin pour récupérer le tout au format EPUB (et un autre pour récupérer le source en Markdown mais c’est un autre sujet).

app/views/news/_news.atom.builder:    epub = content_tag(:div, link_to("Télécharger ce contenu au format EPUB", "#{url}.epub"))
app/views/polls/_poll.atom.builder:  epub = content_tag(:div, link_to("Télécharger ce contenu au format EPUB", "#{url}.epub"))
app/views/posts/_post.atom.builder:  epub = content_tag(:div, link_to("Télécharger ce contenu au format EPUB", "#{url}.epub"))
app/views/nodes/_actions.html.haml:    = link_to "EPUB", "#{path_for_content node.content}.epub", title: "Télécharger ce contenu au format EPUB", class: "action download"
app/views/diaries/_diary.atom.builder:  epub = content_tag(:div, link_to("Télécharger ce contenu au format EPUB", "#{url}.epub"))
app/views/wiki_pages/_wiki_page.atom.builder:  epub = content_tag(:div, link_to("Télécharger ce contenu au format EPUB", "#{url}.epub"))

Côté epub

Le service est plutôt simple, par rapport à img, car il n’a pas de dépendance sur redis par exemple, et qu’il a, au final, peu de paramétrage (un couple adresse+port d’écoute, un fichier de trace et un hôte pour aller chercher les contenus).

Il est possible de faire un GET /status et on obtient une réponse HTTP 200 avec un contenu OK. C’est utile pour tester que le service est lancé (depuis l’intérieur de la plateforme).

Sinon on lui demande une dépêche, un journal, une entrée de forum, un sondage, une entrée de suivi ou une page wiki en prenant le chemin sur LinuxFr.org et ajoutant un petit .epub à la fin, et il va renvoyer un fichier EPUB. Ou bien il va répondre un contenu non trouvé HTTP 404 s’il y a un souci. Et vu son fonctionnement, si on a un souci de HTML non valide ou si img a un problème avec une image, alors derrière epub pourrait avoir le même souci.

epub est un binaire dynamique en Go. Il impose le https pour l’hôte (du coup on aura tous les liens en HTTPS en interne normalement). Il ne peut pas vraiment être compilé statiquement (on a besoin de libxml2, libonig2 et de la même version de la libc au déploiement). Il ne gère pas les images in-line.

Dans les logs on va trouver des infos comme :

2024/11/03 16:34:02 Status code of http:/example.invalid/exemple.png is: 404
(…)
2024/11/03 16:38:23 Fetch https://linuxfr.org/news/capitole-du-libre-2024-au-programme-du-16-et-17-novembre
2024/11/03 16:38:24 Fetch https://linuxfr.org/users/liberf0rce/journaux/libreast-2006-is-out-of-order

Historique

epub a été créé par Bruno Michel en 2013 et Bruno est le seul à travailler dessus (48 commits) jusqu’en 2018. Comme img, on peut considérer que epub a fait le job pendant ce temps-là, sans besoin de retouche.

Mon premier commit de 2021 concerne la gestion d’un cas de collision de nommages des images.

En 2022, Bruno quitte l’équipe du site, et par ailleurs il y a des montées de versions et des migrations à faire sur les serveurs de LinuxFr.org, et epub fait partie des services à reprendre en main. Ce qui veut dire le comprendre, le documenter et au besoin l’améliorer.

Bref je décide de me plonger dans epub (2022-2024), dans la foulée de img, car a priori ce n’est pas un composant compliqué du site (il vit dans son coin, il offre une interface, c’est du Go, donc on a un binaire seulement à gérer - divulgâchage en fait non pas seulement).

Le choix est le même que pour img (cf la dépêche précédente) : ajouter un Dockerfile permettant de recompiler epub dans un conteneur, en contrôlant la version de Go utilisée, en effectuant une détection d’éventuelles vulnérabilités au passage avec govulncheck. Cela me permet de valider que l’on sait produire le binaire d’une part, et que l’on offre à tout le monde la possibilité de contribuer facilement sur ce composant. Et de découvrir qu’une version statique n’est pas facilement envisageable.

Puis je vais tester le composant pour vérifier qu’il fonctionne comme je le pense et qu’il fait ce qu’on attend de lui. Je vais ajouter une suite des tests qui couvrent les différentes fonctionnalités et les vérifient en IPv4 et en IPv6, en HTTP 1.1 et en HTTP 2.0. Les tests utilisent Hurl et docker-compose, et encore une fois l’idée de donner la possibilité de contribuer facilement. Ils comprennent des tests de types de contenus non pris en charge, le test de la limite à 5 MiB, différents types de contenus, le test de vie, des appels erronés (mauvais chemin, mauvaise méthode, etc). Et surtout de vérifier avec epubcheck que le fichier epub produit est correct. Le choix des cas de tests est basé sur le trafic réellement constaté sur le serveur de production, sur les différents cas dans le code et un peu sur l’expérience du testeur.

Les différents travaux effectués vont permettre de détecter et corriger quelques soucis :

Et à la fin, j’écris une dépêche pour parler de tout cela.

Évolutions récentes

Dockerfile

Le fichier Dockerfile du projet permet :

  • de partir d’une image officielle Go d’une version donnée, basée sur une distribution Debian (en raison des dépendances)
  • de l’utiliser pendant la construction en prenant la liste des dépendances de compilation, en les téléchargeant, en prenant l’unique fichier source epub.go et en le compilant dynamiquement avec l’option pour retirer les chemins de compilation
  • de rechercher les éventuelles vulnérabilités avec govulncheck
  • de tester avec golangci/golangci-lint le code (fait à la construction de l’image, car on dispose de toutes les dépendances à ce moment-là)
  • de repartir d’une base Debian en y mettant les autorités de certification, les dépendances de fonctionnement et le binaire issus de la partie construction, de déclarer le port d’écoute et de lancer le binaire avec des variables disposant de valeurs par défaut.

La suite de tests

Pour l’utiliser, c’est assez simple, il faut aller dans le répertoire tests et lancer un docker-compose up --build, qui va produire le conteneur contenant epub, et démarrer le nginx-cert qui fournit les certificats et le nginx préconfiguré pour les tests. Si tout va bien, on attend, et au bout d’un moment il s’affiche :

linuxfr.org-epub-test_1  | All tests look good!
tests_linuxfr.org-epub-test_1 exited with code 0

Rentrons un peu dans les détails.

D’abord un fichier docker-compose.yaml qui décrit le réseau IPv4/IPv6 utilisé pour les tests, l’image nginx-cert qui sera utilisée pour créer une autorité de certification et un certificat serveur de test, l’image nginx qui sera utilisée avec sa configuration et ses fichiers à servir pour les tests, l’image epub et son paramétrage (dont l’accès au nginx) ainsi que le répertoire de l’autorité de certification de tests et enfin l’image de la suite de tests qui est construit avec son Dockerfile et son répertoire de dépôt des fichiers EPUB.

Le Dockerfile de tests est basé sur une image Hurl (un outil pour faire des tests HTTP). On ajoute les fichiers de tests en .hurl, le script shell qui pilote le tout, on prévoit d’avoir les paquets dont on aura besoin : bash (pas par défaut dans les Alpine), curl, openjdk17 (pour epubcheck), openssl, unzip (transitoirement), bind-tools et shellcheck. On installe epubcheck. Et on lance les tests par défaut.

La configuration nginx de test écoute en HTTP sur le port 80 en IPV4 et IPv6 et permet de définir des chemins avec des réponses en HTTP 301, 302, 308, 400, 401, 403, etc. jusqu’à 530 et même 666 pour les codes invalides, ainsi qu’une redirection infinie.

Dans les données de tests servies par nginx, on trouve des contenus du mauvais type, des contenus dans divers formats, une image très grande et des images qui ne seront pas accessibles.

Sont aussi présents deux fichiers de tests avec une extension en .hurl :

  • le test de vie et les chemins hors des contenus autorisés
  • les tests sur les contenus

Vient enfin le script shell qui pilote le tout :

  • on définit les variables pour les cibles IPv4/IPv6 que l’on veut utiliser dans les autres conteneurs Docker
  • on purge le stockage des EPUB sur disque
  • on lance les premiers tests (en IPv4 et IPv6, en HTTP 1.1 et en HTTP 2.0)
  • sur chaque EPUB produit, on lance epubcheck et on regarde si la validation donne le résultat attendu (succès ou échec)
  • si on est arrivé jusque-là on écrit que tout va bien et on déclenche un sourire de satisfaction.

Les problématiques restantes

Il y a quelques entrées encore ouvertes dans le suivi :

  • les images trop grandes (en octet), non récupérables, de format inconnu, etc. : la suite de tests actuelle « couvre » le cas des images de plus de 5 MiB ou non récupérables, avec des tests qui échouent, comme prévu, vu que c’est img qui est censé faire le job de les éviter. Cependant il pourrait être sympa de remplacer toute image non disponible/invalide par une image de remplacement « Image indisponible » du bon Content-Type et du bon nom (vu qu’elle est déclarée dans le MANIFEST).
  • les images trop grandes (en pixel) : globalement on revient à la question des images que laisse passer img
  • les epub non fonctionnels en rédaction et modération : pour des questions de droits, la génération EPUB ne marche pas dans les espaces de rédaction et de modération, à voir si on trouve un contournement ou si on évite de proposer le lien.

Il y a la question habituelle de la montée de versions des dépendances (pour nous actuellement contraintes celles du code Ruby on Rails). Et des questions à se poser sur l’avenir de nginx ?. Les dépendances pendant le fonctionnement amènent aussi leur lot de contraintes.

Conclusion ?

Encore une fois, sans surprise et me répétant, il reste des problématiques et du code à faire pour les gérer (c’est rare un composant sans demandes d’évolution ou de correction). Yapuka (mais probablement plus tard, il faut aussi partager le temps avec les autres composants, ou avoir plus de contributions).

epub rend la fonction que l’on attend de lui, même si on pourrait faire un peu mieux. Plonger dans ce composant s’est avéré assez intéressant et formateur (et nécessaire) : techniquement cela a été l’occasion de faire du Go, du docker et du docker-compose, du nginx, du hurl, de l’HTTP et de gérer des problématiques statique/dynamique et des dépendances. Il s’agissait encore de comprendre ce que faisait un code écrit par une autre personne, de se poser des questions pour choisir les tests et le contenu de la documentation, de se demander pour quelles raisons tel ou tel choix a été fait, de rendre ce composant plus « contribuable », et de compléter le tout de façon détaillée avec une dépêche.

  • # stat d'accès au service

    Posté par  . Évalué à 5 (+4/-0).

    Bonjour,

    Chouette boulot !

    Par curiosité, a-t-on des stats d'accès au service ? Quel pourcentage d'utilisateurs accède au contenu en epub ?
    Si vous en faites partie, sur quel type de contenu utilisez-vous ? Comment faites-vous votre choix ? Utilisez-vous pour lire la première fois ? pour relire déconnecté ? pour archiver ?
    N'étant pas du tout utilisateur de ce format, je me demande si cela ne pourrait pas quand même répondre à certain de mes besoins…

    Bonne journée !

    • [^] # Re: stat d'accès au service

      Posté par  (site web personnel) . Évalué à 4 (+1/-0). Dernière modification le 04 novembre 2024 à 20:47.

      Sur octobre 2024 soit 2,1 M visites et ~25,5M hits au total, mais on va se limite aux requêtes de consultation GET dans la suite :

      • 6,1M hits de flux Atom avec
        • 2,9M pour le flux général des dépêches
        • 1M pour le flux général des journaux
        • 250k pour le flux général des liens
        • 140k pour le flux général des forums
        • 32k pour le flux général des sondages
        • puis viennent en vrac les flux des comptes, des commentaires sur les contenus, de la rédaction, des pages wiki, des étiquettes, des sections
      • 6,0M hits sur la page d'accueil
      • 2,6M hits d'images (png, jpeg, gif, svg, webp)
      • 0,9M hits sur la tribune libre + 34k pour la rédaction + 20k pour la modération
      • 476k hits de fontes woff2
      • 320k hits de CSS
      • 310k hits de favicon + 33k de favicon.ico
      • 238k hits de javascript
      • 155k hits sur la page d'accueil des dépêches
      • 116k hits sur la page d'accueil des journaux
      • 70k hits sur la page d'accueil des liens
      • 66k hits de robots.txt
      • 58k hits d'accès aux statistiques webalizer
      • 40k hits sur la page d'accueil des forums
      • 10k hits sur la page d'accueil du wiki
      • 27k hits sur le plan du site
      • 15k hits sur l'aide du site
      • 215k hits de source markdown (entre 0 et 422 hits pour un contenu sur ce mois)

      • 350k hits de epub (entre 0 et 489 hits pour un contenu sur ce mois)

      • dépêche la plus affichée individuellement 14k hits

      • journal le plus affiché individuellement 5k hits

      • entrée de forum la plus affichée individuellement 1,4k hits

      Quel pourcentage d'utilisateurs accède au contenu en epub ?

      On ne sait pas, ce n'est pas une information stockée (on ne sait pas quel compte ou quel Anonyme accède à quoi, en dehors de l'adresse IP qui a demandé une page).

    • [^] # Re: stat d'accès au service

      Posté par  (site web personnel, Mastodon) . Évalué à 3 (+0/-0).

      N'étant pas du tout utilisateur de ce format, je me demande si cela ne pourrait pas quand même répondre à certain de mes besoins…

      Je ne sais pas quels sont les besoins en question. En ce qui me concerne, propriétaire d’une liseuse, j’utilise pas mal le format EPUB (pas uniquement pour LinuxFR) pour pouvoir lire tranquillement sur ma liseuse en déconnecté quand je travaille sur un sujet. En outre, ça permet de faire des copier-coller propres et, évidemment, on peut ajouter un article dans Zotero pour une bibliographie tout en utilisant le format EPUB pour une lecture déconnectée.

      Il y a une extension pour Firefox, Save as ebook (enregistrer comme ebook) qui permet de générer facilement des EPUB à partir d’articles de sites. Pour LinuxFR cette extension permet de récupérer le texte du contenu et les commentaires.

      Selon les sites et le sujet, je lis vraiment l’article, je le parcours ou je l’ajoute simplement comme EPUB. Bref, ça dépend.

      « Tak ne veut pas quʼon pense à lui, il veut quʼon pense », Terry Pratchett, Déraillé.

  • # typo

    Posté par  . Évalué à 4 (+2/-0).

    LinuxFr.org vous permet de libre les contenus

    de lire.

  • # EPUB 3 ou MP3

    Posté par  (site web personnel) . Évalué à 1 (+1/-0).

    Petite précision.

    Un logiciel capable de lire un fichier EPUB 3 ne fait que l'afficher (techniquement c'est l'humain qui lit ensuite le livre électronique affiché).

    Pour lire un livre électronique, il faut que ce soit un livre audio au format MP3.

    Thorium Reader est capable de faire les deux (afficher un fichier EPUB 3 et lire des fichiers audio au format MP3).

    En théorie, les écrans bleux sont rare. En pratique, les utilisateurs appellent rarement le support pour dire : Bonjour.

  • # Idée pour Hurl

    Posté par  . Évalué à 4 (+4/-0).

    Salut, vraiment un article encore très intéressant !

    Je suis l'un des mainteneurs de Hurl et j'ai regardé les tests Hurl par curiosité:

    GET http://{{TARGET}}:9000/news/news_slug.epub
    [Options]
    http2: {{HTTP2}}
    output: epub/news_slug.epub
    HTTP/1.1 200
    [Asserts]
    header "Content-Type" == "application/epub+zip"
    bytes contains hex,6D696D65747970656170706C69636174696F6E2F657075622B7A6970; # mimetypeapplication/epub+zip
    bytes contains hex,4D4554412D494E462F636F6E7461696E65722E786D6C; # META-INF/container.xml
    bytes contains hex,455055422F6E61762E7868746D6C; # EPUB/nav.xhtml
    bytes contains hex,455055422F526F6E526F6E6E656D656E742E637373; # EPUB/RonRonnement.css
    bytes contains hex,455055422F636F6E74656E742E7868746D6C; # EPUB/content.xhtml
    bytes contains hex,455055422F7061636B6167652E6F7066; # EPUB/package.opf
    

    Il n'y a rien dans Hurl pour aider à tester du contenu d'un fichier zip et effectivement ça rend les tests un peu fastidieux à écrire (d'où la présence des commentaires qui indiquent quels fichiers on attend dans l'archive). Dans Hurl, il y a la notion de filtres et je me demande si on ne pourrait pas implémenter un filtre "zipfiles" (nom à trouver!) qui renvoie la liste des noms de fichiers de l'archive.

    Le test deviendrait quelque chose comme ça (avec le filtre nth qui permet d'accéder à un élément de liste):

    GET http://{{TARGET}}:9000/news/news_slug.epub
    [Options]
    http2: {{HTTP2}}
    output: epub/news_slug.epub
    HTTP 200
    [Asserts]
    header "Content-Type" == "application/epub+zip"
    bytes zipfiles nth 0 "mimetypeapplication/epub+zip"
    bytes zipfiles nth 1 "META-INF/container.xml"
    bytes zipfiles nth 2 "EPUB/nav.xhtml"
    bytes zipfiles nth 3 "EPUB/RonRonnement.css"
    bytes zipfiles nth 4 "EPUB/content.xhtml"
    bytes zipfiles nth 5 "EPUB/package.opf"
    

    Ca pourrait être sympa, on va en discuter entre mainteneurs! En tout cas, n'hésitez pas à nous remonter des issues ou des idées d'améliorations sur Hurl, on est preneurs (dans la limite des choses réalisables :))!!

    • [^] # Re: Idée pour Hurl

      Posté par  (site web personnel) . Évalué à 5 (+2/-0). Dernière modification le 08 novembre 2024 à 10:22.

      Bonne idée le filtre zipfiles.

      De ma petite expérience avec Hurl (que j'ai utilisé uniquement pour img et epub pour LinuxFr.org donc), il y a deux autres points qui m'intéresseraient :

      • un moyen d'appliquer un text-to-hexa / hexa-to-text à une variable (j'ai des GET http://{{TARGET}}:8000/img/687474703A2F2F{{WEB_HEX}}2F6E6F6E6578697374696E67 que je pourrais rendre plus clairs et me dispenser d'un commentaire). Avec un biais ansible, je dirais pouvoir faire un {{ MYVAR | text_to_hexa }} par exemple (en l'occurrence "https://" + MYVAR + "/non-existing" le tout passé à un filtre text_to_hexa). Au passage j'ai noté que plus ou moins involontairement j'ai mis des majuscules ou des minuscules dans mon hexadécimal dans les tests, ce qui serait aussi évité avec text_to_hexa(uppercase) / text_to_hexa(lowercase)). Quelque chose qui ressemble à https://github.com/Orange-OpenSource/hurl/issues/2258 et au concept de generator apparemment.
      • un moyen de plus réutiliser le code d'un test, par exemple seulement faire varier la méthode et l'URL visée dans mon cas. Quelque chose qui ça ressemble à https://github.com/Orange-OpenSource/hurl/issues/2950 je dirais.

      Je peux faire les deux avec du shell autour en gérant mes appels à hurl (en pré-calculant la variable et en faisant une boucle shell) mais si je pouvais rester en pur hurl, je me dis que ça serait plus sympa.

      • [^] # Re: Idée pour Hurl

        Posté par  . Évalué à 2 (+2/-0).

        Merci pour le retour, pour le 1er point, les "générateurs" ça va arriver très prochainement et effectivement cela permettra de rendre certains tests plus lisibles. Pour le deuxième point, c'est plus dur, on hésite très fortement à introduire de la complexité mais en contrepartie le langage peut paraître limité (limitant?)… A suivre !

    • [^] # Re: Idée pour Hurl

      Posté par  . Évalué à 3 (+1/-0).

      Salut, petite question sans rapport avec le journal, quel est le rapport de Hurl avec Orange ? Il me semblait que le dépôt d'origine était celui-là

Envoyer un commentaire

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.