J’ai arrêté de faire mes pg_dump à la main (et ma base de 77 Go me dit merci)

Rappelez-vous, il y a quelques années, j’avais parlé de la migration de la base de mon jeu AIM sur PostgreSQL. C’était une bonne idée, mais je n’avais rien automatisé au niveau des sauvegardes. Donc je faisais régulièrement mes sauvegardes à la main, je coupais le jeu et hop, un bon dump qui prenait trois heures.

Puis j’ai fait le calcul du nombre de jours de données que je perdrais si le disque lâchait maintenant. Et j’ai arrêté de trouver ça drôle. Parce que perdre 15 jours de jeu, ça peut faire pleurer.

Voilà donc le récit de comment je suis passée du pg_dump artisanal à une vraie automatisation avec pgBackRest, ce que ça m’a appris sur les enjeux réels d’une sauvegarde, et surtout ce que tout ça coûte (ou pas) aux performances du SGBD.


Le problème du dump manuel : ce n’est pas une sauvegarde, c’est un souvenir

Commençons par le mythe à démonter. Beaucoup de gens (moi la première) pensent avoir une sauvegarde parce qu’ils ont ça quelque part :

pg_dump -U postgres mabase > /backup/mabase.sql

C’est un fichier. Ce n’est pas une stratégie. Voici pourquoi.

1. Le RPO est catastrophique

Le RPO (Recovery Point Objective), c’est la quantité de données que tu acceptes de perdre. Avec un dump manuel fait « de temps en temps », ton RPO c’est : depuis la dernière fois que j’y ai pensé. Ça peut être trois semaines. Et tu ne le sauras qu’au moment où ça comptera vraiment.

2. Un dump de 77 Go, ce n’est pas un fichier, c’est un événement

Ma base de prod fait environ 77 Go. Un pg_dump là-dessus, ce n’est pas une commande qu’on lance négligemment en journée : c’est une longue lecture logique de toute la base, un CPU qui part en compression, et un fichier énorme à écrire. Je vais y revenir dans la partie perfs, mais retenez déjà ceci : plus la base grossit, moins tu as envie de lancer ta sauvegarde à la main. Et donc moins tu la lances. Le manuel se sabote tout seul.

3. Tu ne peux pas remonter à hier 14h37

Le dump est une photo. Si ta dernière photo date de dimanche et que la bêtise a eu lieu mardi à 14h37, tu reviens à dimanche. Point. Toutes les données de lundi et mardi : perdues. Alors que PostgreSQL sait faire infiniment mieux — il écrit en permanence un journal de toutes ses modifications, les WAL (Write-Ahead Logs). Si on archive ces journaux, on peut rejouer l’histoire jusqu’à la minute près. C’est le PITR (Point-In-Time Recovery). Le dump manuel jette cette possibilité à la poubelle.

4. Une sauvegarde jamais restaurée n’existe pas

C’est le point qui fait le plus mal. Un dump dans un coin, dont tu n’as jamais vérifié qu’il se restaure, c’est un pari, pas une garantie. L’automatisation force à structurer ça : un dépôt, un inventaire, une commande de vérification.


Ce que j’ai mis en place : pgBackRest + archivage des WAL

J’ai choisi pgBackRest. Pas par snobisme d’outil, mais parce qu’il fait nativement les trois choses que je voulais : sauvegarde physique, archivage WAL, et rétention automatique.

Étape 1 — Autoriser l’archivage côté PostgreSQL

Dans postgresql.conf :

wal_level = replica
archive_mode = on
archive_command = 'pgbackrest --stanza=mabase archive-push %p'
max_wal_senders = 3

Le point important ici : wal_level = replica est le minimum requis. Si vous laissez minimal, PostgreSQL refusera carrément de démarrer avec archive_mode = on. J’ai eu droit à ce joli message, un soir :

FATAL: WAL archival cannot be enabled when wal_level is "minimal"

Ça a le mérite d’être clair.

Étape 2 — Configurer la stanza

Une stanza, chez pgBackRest, c’est simplement le nom qu’on donne à un cluster sauvegardé. La mienne s’appelle mabase.

Dans /etc/pgbackrest.conf :

[global]
repo1-path=/var/lib/pgbackrest
repo1-retention-full=2
compress-type=lz4
process-max=4
 
[mabase]
pg1-path=/var/lib/postgresql/16/main

Trois choix à souligner :

  • compress-type=lz4 : nettement plus rapide que gzip pour un taux de compression très honnête. Sur 77 Go, la différence n’est pas cosmétique.
  • process-max=4 : pgBackRest parallélise. C’est un de ses gros avantages sur pg_dump, qui reste largement mono-thread sur ce type de travail.
  • repo1-retention-full=2 : je garde deux sauvegardes complètes, les vieilles s’effacent toutes seules. La rétention automatique, c’est la moitié de l’intérêt de l’automatisation. Sans elle, on remplace « oublier de sauvegarder » par « saturer le disque de sauvegardes ». J’ai vu des serveurs mourir de ça.

Étape 3 — Initialiser et vérifier

sudo -u postgres pgbackrest --stanza=mabase stanza-create
sudo -u postgres pgbackrest --stanza=mabase check

Le check est votre ami : il valide que l’archivage WAL fonctionne réellement de bout en bout. Ne sautez pas cette étape.

Étape 4 — Le cron (le cœur du sujet)

crontab -u postgres -e
0 2 * * 2 nice -n 10 pgbackrest --stanza=mabase --type=full backup

Traduction : tous les mardis à 2h du matin, sauvegarde complète, en priorité CPU basse.

Deux détails qui comptent :

  • Le cron appartient à l’utilisateur postgres, pas à root. C’est le bon niveau de privilège pour ce travail — inutile de faire tourner en root quelque chose qui n’en a pas besoin.
  • nice -n 10 : la sauvegarde s’exécute en priorité réduite. Si le serveur a autre chose à faire à 2h du matin, la sauvegarde s’efface poliment. On y revient tout de suite.

Et pour vérifier au réveil que tout s’est bien passé :

sudo -u postgres pgbackrest --stanza=mabase info

Et les performances du SGBD dans tout ça ?

C’est LA question qui m’a fait traîner des mois. « Si j’automatise, est-ce que je vais plomber ma base ? »

Réponse courte : le manuel coûte plus cher que l’automatique. Voici pourquoi.

pg_dump est une lecture logique, pgBackRest une copie physique

pg_dump ne copie pas des fichiers : il reconstruit la base en la relisant intégralement et en générant du SQL. Ça veut dire un parcours complet, du CPU en pagaille, et surtout une transaction très longue ouverte pendant toute l’opération. Or une transaction longue empêche l’autovacuum de nettoyer les lignes mortes derrière elle. Sur une base à fortes écritures, un dump interminable en pleine journée, c’est du bloat qui s’accumule et des perfs qui se dégradent — bien après la fin du dump.

pgBackRest, lui, copie les fichiers du cluster. C’est plus proche du disque, ça se parallélise, et ça n’a pas ce coût transactionnel.

Le vrai chiffre

Ma première sauvegarde complète des 77 Go, avec lz4 et nice -n 10, a pris 22 minutes. Vingt-deux minutes, à 2h du matin, en priorité basse, sans que personne ne s’en aperçoive. À comparer avec un dump équivalent lancé un mardi à 15h « parce que j’y ai pensé ».

Le coût continu de l’archivage WAL

L’archivage n’est pas gratuit : chaque segment WAL rempli déclenche un archive-push. Le coût unitaire est faible, mais il y a un piège à connaître : si l’archive_command est plus lente que le rythme de production des WAL, les segments s’accumulent dans pg_wal/ et le disque se remplit. Surveillez cet espace disque. C’est le principal effet de bord d’une config PITR, et il est silencieux jusqu’au moment où il ne l’est plus.

Un mot sur synchronous_commit

Sur des charges à écritures très fréquentes, synchronous_commit = off donne un gain net : PostgreSQL n’attend plus la confirmation d’écriture du WAL sur disque pour valider la transaction. Mais soyons précises sur ce qu’on échange : en cas de crash brutal, vous perdez les quelques dernières centaines de millisecondes de transactions validées. La base reste cohérente (ce n’est pas fsync = off, qui lui est un vrai danger en prod), mais elle n’est pas complète.

C’est un arbitrage à faire en conscience, selon la valeur de vos dernières millisecondes.


Le jour où ça plante : restaurer à mardi 14h37

Tout ce qui précède ne sert à rien si on ne sait pas s’en servir le jour J. Alors imaginons le scénario. Mardi, 14h37, quelqu’un lance un DELETE sans WHERE sur une table centrale. À 14h50, on s’en aperçoit. Objectif : remettre la base exactement dans l’état de 14h36.

0. Ne rien faire de brutal

Le premier réflexe est le plus important : on ne redémarre pas PostgreSQL, on ne bricole rien. Si la base tourne encore, on l’arrête proprement et on réfléchit. Les WAL déjà archivés sont votre matière première : ne les touchez pas.

1. Regarder ce dont on dispose

sudo -u postgres pgbackrest --stanza=mabase info

Vous obtenez la liste des sauvegardes complètes avec leurs dates de début et de fin. Le point à vérifier absolument : votre cible (mardi 14h36) doit être postérieure à la fin d’une sauvegarde complète, et tous les WAL entre cette sauvegarde et la cible doivent être présents dans l’archive. Dans mon cas, avec un full le mardi à 2h, une cible à 14h36 le même mardi est parfaitement atteignable : pgBackRest repart du backup de 2h et rejoue ~12h de WAL.

2. Arrêter PostgreSQL

sudo systemctl stop postgresql

3. Restaurer avec une cible temporelle

C’est ici que le PITR prend tout son sens :

sudo -u postgres pgbackrest \
  --stanza=mabase \
  --delta \
  --type=time \
  --target="2026-07-14 14:36:00+02" \
  --target-action=promote \
  restore

Décortiquons, parce que chaque option compte :

  • –type=time + –target : la date et l’heure exactes où l’on veut atterrir. Précisez le fuseau horaire (+02 ici) — sans lui, PostgreSQL interprète selon le timezone du serveur, et se tromper d’une heure sur un rollback, c’est ballot.
  • –delta : pgBackRest compare les fichiers existants et ne réécrit que ce qui diffère. C’est plus rapide, et surtout ça vous évite d’avoir à vider le répertoire de données à la main. Parce que oui, la méthode « officielle » sans –delta exige un data directory vide, et ça veut dire taper un rm -rf /var/lib/postgresql/16/main/* en tremblant à 15h un mardi. Utilisez –delta.
  • –target-action=promote : une fois la cible atteinte, PostgreSQL sort du mode recovery et redevient ouvert en écriture. L’alternative, –target-action=pause, arrête la relecture et attend : la base est lisible mais figée, ce qui permet de vérifier qu’on est bien au bon endroit avant de valider. Sur une restauration où j’ai un doute sur l’heure exacte, je commence par pause.

pgBackRest choisit tout seul la bonne sauvegarde complète à utiliser en fonction de la cible. Il écrit également le recovery.signal et le restore_command dans postgresql.auto.conf — inutile de les créer à la main.

4. Redémarrer et regarder la base rejouer l’histoire

sudo systemctl start postgresql
sudo journalctl -u postgresql -f

C’est le moment le plus étrange de l’opération : PostgreSQL rejoue les WAL un par un, et vous voyez défiler dans les logs les heures de votre journée à l’envers. Vous cherchez la ligne qui confirme que le point de recovery a été atteint (recovery stopping before commit of transaction…, puis database system is ready to accept connections).

Tant que le recovery tourne, la base est en lecture seule. C’est normal, laissez-la finir.

5. Vérifier avant de crier victoire

La donnée la plus récente doit dater de ~14h36. Si oui : c’est gagné, et vous venez de récupérer 12h de travail qu’un pg_dump du dimanche vous aurait fait perdre.

SELECT COUNT(*) FROM ma_table_massacree;
SELECT MAX(created_at) FROM ma_table_massacree;

Pour conclure

Voilà tout tourne en tâche de fond, et les joueurs et moi on n’a plus à s’en soucier et au lieu de se préoccuper des dumps on peut ainsi écouter Radio Chirac, un son qui déchire pas mal.

De la cybersécurité (I’m back. Stronger.)

Enfin j’ai trouvé un peu de temps pour réparer tout mon bourbier.

En effet, depuis 2024, quand on accédait à mes sites wordpress, vous aviez ce message d’accueil fort charmant qui mentionnait un « incident technique ». J’avais dû fermer mes trois sites wordpress …

Pas des moindres. J’avais été piratée de partout ! Mon site d’urbex par exemple redirigeait vers une boutique en ligne. Et paf la cascade, mon site vitrine, mon blog et même un temps mon wiki sur mon jeu Minerland ! Un peu comme un jeu de dominos qu’on fait tomber petit à petit … de quoi être parano.

Il aura fallu attendre 2024 pour que ça se produise, en sachant que mes premiers sites perso remontent à 2000. Depuis, je me disais, tranquille, que je n’avais pas de raisons de me méfier ni de consolider mes iptables bien comme il le faut … voire faire l’impasse sur les mises à jour. L’erreur fatale. La cybersécurité est devenue lame de plus du développeur web qui doit apprendre de ses erreurs.

Intro sur la cybersécurité

En 2026, les petits sites web sont plus que jamais en première ligne.

Les attaques les plus courantes contre les petits sites sont silencieuses, opportunistes et automatisées. Elles défigurent un site, volent des données clients, ou utilisent le serveur pour héberger des contenus illicites, le tout à l’insu du propriétaire.

La grande nouveauté de 2026, c’est l’utilisation de l’IA par les attaquants. Les agents IA autonomes deviennent des outils courants de la cybercriminalité, capables de cartographier une attaque en quelques minutes et de générer des campagnes de phishing ciblées à grande échelle. Les campagnes de phishing générées par IA sont désormais quasiment indétectables — fini les fautes d’orthographe et les formulations bancales.

Plus de 90 % des piratages de sites WordPress exploitent des failles déjà corrigées dans une mise à jour que l’administrateur n’a pas appliquée. Le message est clair : mettre à jour, sécuriser ses accès, surveiller ses logs — ce n’est plus optionnel, même pour un petit blog perso.

Acte I — La découverte

Tout a commencé par un constat simple : mes sites WordPress ne répondaient plus correctement. En fouillant, j’ai découvert l’ampleur des dégâts. Les attaquants avaient réussi à s’introduire dans mes trois sites WordPress et ils s’étaient fait plaisir.

Leur spécialité : injecter du contenu. Des pages de spam, du phishing, et le pompon — un site de commerce en ligne complet, hébergé à mes frais sur mon propre serveur. Sympa.

En analysant les bases de données, j’ai retrouvé leurs traces partout. Dans la base d’amelieonline.net, un webshell — un script PHP qui permet de prendre le contrôle d’un serveur à distance — était planqué dans le fichier functions.php du thème. Du code obfusqué en base64 qui se connectait à des serveurs distants pour exécuter des commandes. Le plugin de sécurité All-In-One Security l’avait détecté et mis en quarantaine, mais le mal était fait.

Dans les fichiers du serveur, les bots avaient déposé des scripts PHP aux noms discrets : x.php, ha.php, gg.php… Des portes dérobées qui leur permettaient de revenir à volonté, même après un changement de mot de passe.

Acte II — Comment ils sont rentrés

La réponse est aussi simple que gênante : mon mot de passe wordpress était devenu trop faible. Ou alors ils se sont fait une joie de l’absence de certificat ssl …

Les bots qui parcourent internet 24h/24 testent des millions de combinaisons sur /wp-login.php et xmlrpc.php (une porte d’entrée WordPress souvent oubliée). Avec un mot de passe simple, il leur faut parfois quelques heures pour entrer.

Mais ce n’est pas tout. Mon serveur cumulait les facteurs aggravants. Ubuntu 18.04 en fin de vie depuis avril 2023, donc plus aucun correctif de sécurité. PHP 7.x, qui n’est plus maintenu depuis fin 2022. Pas de HTTPS — ce qui signifie que chaque fois que je me connectais à l’admin WordPress, mon identifiant et mon mot de passe transitaient en clair sur le réseau. Dans un café, à la gare, à l’hôtel, n’importe qui sur le même Wi-Fi pouvait les intercepter avec un simple outil comme Wireshark.

Et mon certificat SSL ? Expiré depuis des mois. J’avais payé cher pour un certificat classique, il avait expiré, et je n’avais jamais pris le temps de le renouveler. J’étais persuadée qu’il fallait racheter un certificat pour chaque nom de domaine. Trois domaines, trois certificats, une facture qui pique. En fait, non — mais j’y reviendrai.

Acte III — Le grand nettoyage

Le ménage a été méthodique. D’abord, les bases de données : analyser chaque dump SQL à la recherche de contenu injecté — du spam en chinois, en japonais, en cyrillique, des liens vers des boutiques frauduleuses, des webshells en quarantaine. Chaque site a été passé au peigne fin, table par table, grâce à l’aide de mon ami Claude, devenu mon nouveau formateur/secrétaire/IA couteau suisse.

Ensuite, les fichiers du serveur : traquer les scripts PHP malveillants dans les répertoires d’uploads (il ne devrait jamais y avoir de fichier PHP dans wp-content/uploads/), vérifier chaque thème, chaque plugin, identifier les fichiers modifiés récemment.

Et puis la grosse opération : migrer le serveur d’Ubuntu 18.04 vers une version maintenue. Trois sauts successifs — 18.04 vers 20.04, puis 22.04 — chacun avec son lot de surprises. PHP 7 vers PHP 8 a cassé plusieurs choses : des fonctions dépréciées, des syntaxes devenues invalides, des conflits de configuration entre l’ancien PHP-FPM et le nouveau module. À chaque étape, un site qui tombe, un diagnostic, une correction, on repart.

Acte IV — L’horreur des logs

C’est une fois le serveur remis d’aplomb que j’ai vraiment mesuré l’ampleur du problème. En regardant les logs d’authentification SSH :

Failed password for root from 179.111.150.107
Failed password for invalid user adurbex from 172.188.89.41
Failed password for invalid user amelayesbiophp from 59.51.98.254
Failed password for invalid user urbexmarseille from 187.191.48.4
Failed password for invalid user tonwebautableau from 121.78.158.30

Des dizaines de tentatives par heure. Et le plus flippant : les bots n’essayaient pas que des noms génériques comme « admin » ou « root ». Ils testaient adurbex, urbexmarseille, amelayesbiophp etc … — des identifiants dérivés de mes noms de domaines, de mon pseudo, de mes projets.

Quelqu’un — ou plutôt quelque chose — avait scanné mes sites, relevé tous les indices disponibles. Les noms d’auteur WordPress sont exposés par défaut via /?author=1 ou l’API REST (/wp-json/wp/v2/users). Les pages « à propos », les footers, les noms de domaine eux-mêmes : tout est découpé, recombiné, et testé automatiquement.

Les logs Apache racontaient la même histoire côté web : des scans massifs de wlwmanifest.xml (un fichier WordPress utilisé pour détecter les installations), des rafales de requêtes cherchant des webshells PHP à des noms comme x.php, ha.php, gg.php… Un bot a généré des dizaines de 404 en quelques millisecondes, balayant méthodiquement tous les noms de fichiers suspects.

En analysant les adresses IP, un inventaire géographique impressionnant : Brésil, Bulgarie, Chine, Corée du Sud, Russie, Vietnam, Pays-Bas, Ukraine, Singapour, Afrique du Sud… La plupart provenaient de datacenters ou de services cloud comme Microsoft Azure ou Alibaba Cloud. Et l’ironie : l’une des IP attaquantes appartenait à un serveur Kimsufi OVH — un serveur dédié comme le mien, probablement compromis et utilisé comme relais.

Ce ne sont pas de vrais humains derrière un écran. Ce sont des botnets — des réseaux de milliers de machines compromises qui scannent automatiquement toutes les IP du monde, 24 heures sur 24.

Acte V — Pourquoi mon petit serveur ?

C’est la question que je me suis posée. Mes sites ne contiennent rien de sensible. Mais ce n’est pas mes données qui intéressent les attaquants. C’est mon serveur lui-même : sa bande passante pour lancer des attaques vers d’autres cibles ou participer à des attaques DDoS, son processeur pour miner de la cryptomonnaie, son adresse IP pour héberger du phishing ou envoyer des millions de spams, et le fait qu’il tourne 24h/24 en faisant office d’outil gratuit.

Un serveur dédié compromis devient un soldat anonyme dans une armée de machines zombies. Et quand quelqu’un remonte la piste d’une attaque, c’est sur mon Kimsufi qu’il tombe — pas sur le vrai attaquant.

Acte VI — La fortification

La reconstruction s’est faite couche par couche.

Fail2ban, valeur sûre. J’avais fail2ban d’installé, mais sa configuration par défaut était une passoire : 10 minutes de bannissement après 5 échecs. Les bots attendaient tranquillement et revenaient. 4681 tentatives échouées au compteur, 491 IP bannies au total, mais zéro bannie à l’instant T. J’ai durci la config : 3 échecs en une heure, et c’est 24 heures de ban.

Mais le SSH n’était pas le seul front. J’ai ajouté des jails fail2ban pour chaque type d’attaque : les scans WordPress (wlwmanifest), les floods de 404 génériques, les tentatives de brute force sur DokuWiki, les scans de fichiers sensibles. Six jails au total, chacune avec ses propres seuils. Le port 22 quant à lui, exit !

Iptables pour les récidivistes. Pour les IP les plus agressives — celles avec 60, 80, 90 tentatives au compteur — j’ai écrit un script qui les bloque définitivement dans iptables. Ces adresses ne peuvent plus du tout communiquer avec mon serveur, sur aucun port, par aucun protocole.

CrowdSec, la défense collaborative. En plus de fail2ban, j’ai installé CrowdSec — un outil communautaire où les serveurs partagent les IP malveillantes. Quand une IP attaque un serveur du réseau, elle est signalée et bloquée sur tous les autres.

HTTPS avec Let’s Encrypt. Fini les certificats payants. Certbot et Let’s Encrypt m’ont permis de sécuriser mes trois domaines en une seule commande, gratuitement, avec renouvellement automatique tous les 90 jours. Plus jamais de certificat expiré oublié dans un coin. Un timer systemd vérifie deux fois par jour si un renouvellement est nécessaire.

Le blocage de xmlrpc.php. Ce fichier WordPress est un vecteur classique de brute force. Bloqué.

Les permissions qui vont bien. 644 pour les fichiers, 755 pour wp-content.

La mise à jour de tout. Ubuntu à jour, PHP à jour, WordPress core à jour, plugins à jour, thèmes à jour. Chaque composant obsolète est une porte ouverte.

Ce que j’ai appris

Cette aventure m’a enseigné des leçons que j’aurais aimé connaître plus tôt.

Un mot de passe faible est une porte ouverte. C’est banal à dire, mais c’est la cause numéro un des compromissions WordPress. Aujourd’hui, 20 caractères minimum, générés aléatoirement, et uniques pour chaque site.

Le HTTPS n’est pas optionnel. Même pour un blog perso, chaque page de connexion sans HTTPS est une invitation au vol d’identifiants. Et avec Let’s Encrypt, il n’y a plus aucune excuse financière.

La sécurité par défaut n’existe pas. Fail2ban installé avec sa config d’usine, c’est presque comme s’il n’était pas là. Il faut prendre le temps de configurer chaque outil correctement.

Tout serveur exposé sur internet est une cible. Ce n’est pas une question de « si », mais de « quand ». Les bots ne dorment jamais, et ils sont plus malins qu’on ne le croit — ils analysent vos sites pour en extraire des identifiants potentiels. Les premières tentatives de brute force arrivent dans les minutes qui suivent la mise en ligne d’un serveur.

Un système non maintenu est un système compromis. Ubuntu 18.04 en fin de vie + PHP 7 abandonné = zéro correctif de sécurité depuis des mois. Autant laisser la porte grande ouverte.

Vérifiez vos logs régulièrement. Sans cette habitude, je n’aurais jamais su que mon serveur subissait des milliers de tentatives d’intrusion quotidiennes.

Migration du serveur Minetest (en PostgreSQL !)

Le moment était venu. Le serveur qui hébergeait jusqu’alors mon jeu Minetest « Amelaye in Minerland » commençait à souffrir, tant la base de données tendait à être énorme (33 Go, vous voyez un peu …). Difficile de dire aux joueurs d’arrêter d’explorer, de creuser, bref de tirer la plus-value du monde qu’ils aimaient modifier. D’autant plus que j’avais bien envie d’installer de nouveaux mods. Et puis surtout, le serveur qui héberge le jeu est une machine perso, qui sert également pour pas mal de choses.

Alors j’ai décidé d’une part de louer un nouveau serveur Kimsufi, une machine assez robuste pour cet usage exclusif, et d’autre part, de migrer la base de données Minetest, à l’origine en SQLite vers PostgreSQL. Pourquoi ? Parce que si on regarde bien, c’est plutôt vivement recommandé.

Documentation des backends Minetest

Tout est dit, ne plus rester sous SQLite implique de limiter les lags, et également de réduire les risques de corruption de la base de données (ma plus grande crainte !). Et ça, c’est tout bénéf’ !

Allons-y. Ma nouvelle machine arrive toute fraiche avec la dernière version d’Ubuntu. Déjà, je m’enlève de la tête qu’on n’installe pas Minetest avec un simple apt-get dans mon cas de figure, ce serait trop simple. Car la version déjà compilée ne supporte pas PostgreSQL. Il va falloir compiler les sources. Je relève le défi.

On prépare le terrain

J’installe la matière première, les librairies qui me seront utiles …

sudo apt install wget postgresql 
postgresql-contrib g++ make libc6-dev libirrlicht-dev cmake 
libbz2-dev libpng-dev libjpeg-dev libxxf86vm-dev libgl1-mesa-dev 
libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev 
libcurl4-gnutls-dev libfreetype6-dev zlib1g-dev libgmp-dev 
libjsoncpp-dev libpq-dev libpqtypes-dev doxygen 
libluajit-5.1-dev libspatialindex-dev libncurses-dev

Comme ça, c’est fait. Je me crée un utilisateur minetest :

sudo useradd -mU minetest

Et je me logue sur le compte, comme ça :

sudo -i -u minetest

Puis je télécharge les sources de Minetest. Important : téléchargez la DERNIERE VERSION stable qui correspond à celle que vous utilisiez, sinon ça peut engendrer des bugs. Dans mon cas, c’est la 5.5.1 :

git clone https://github.com/minetest/minetest.git
git checkout 5.5.1
cd minetest/games/
git clone https://github.com/minetest/minetest_game.git
git checkout 5.5.1
cd ..

Ok, c’est facile. Maintenant, je passe à la pré-compilation (et je croise les doigts) :

cmake . -DRUN_IN_PLACE=TRUE 
-DBUILD_CLIENT=FALSE 
-DBUILD_SERVER=TRUE 
-DCMAKE_BUILD_TYPE=Release 
-DBUILD_UNITTESTS=FALSE 
-DENABLE_POSTGRESQL=TRUE 
-DVERSION_EXTRA=postgresql 
-DPostgreSQL_INCLUDE_DIR=/usr/include/postgresql 
-DPostgreSQL_TYPE_INCLUDE_DIR=/usr/include/postgresql 
-DPostgreSQL_LIBRARY=/usr/lib/x86_64-linux-gnu/libpq.so

Et là … ça plante.

CMake Error at CMakeLists.txt:110 (message):
IrrlichtMt headers are required to build the server, but none found.
The Minetest team has forked Irrlicht to make their own customizations.
It can be found here: https://github.com/minetest/irrlicht
For example use: 
git clone --depth=1 https://github.com/minetest/irrlicht lib/irrlichtmt

On sent bien les origines allemandes du jeu, tiens. Il a besoin d’une librairie « Irrlicht » pour pouvoir compiler. Mais pourtant, je l’ai apt-getté, celui-là … bon, on va là aussi compiler les sources avec l’adresse GIT qui est fournie alors.

Installation d’Irrlicht

Donc, on récupère Irrlicht dans le répertoire minetest pour pouvoir faire en sorte que l’include dans les sources trouve ses petits. Il est très important d’installer Irrlicht dans lib/irrlichtmt

git clone --depth=1 https://github.com/minetest/irrlicht lib/irrlichtmt

On va dans le répertoire et on compile :

cd lib/irrlichtmt
cmake . -DBUILD_SHARED_LIBS=OFF
make -j$(nproc)

Normalement on s’en sort avec juste quelques warnings.

Compilation réussie !

Je reviens deux répertoires en arrière, relance la pré-compilation de Minetest, et … mince encore des erreurs.

CMake Error at /usr/share/cmake-3.22/Modules/FindPackageHandleStandardArgs.cmake:230 (message):
Could NOT find Zstd (missing: ZSTD_LIBRARY ZSTD_INCLUDE_DIR)
Call Stack (most recent call first):
/usr/share/cmake-3.22/Modules/FindPackageHandleStandardArgs.cmake:594 (_FPHSA_FAILURE_MESSAGE)
cmake/Modules/FindZstd.cmake:24 (find_package_handle_standard_args)
src/CMakeLists.txt:228 (find_package)

Bon, certainement une lib qui manque, ce ne sera pas la mer à boire. Je reviens dans mon compte utilisateur et je fais un petit :

sudo apt install -y libzstd-dev

Je reviens dans mon compte minetest et mon répertoire minetest, je relance la pré-compilation. Cool, ça a marché. Maintenant je lance la compilation :

make -j$(nproc)

Okay, le terminal m’indique que minetestserver a été compilé avec succès.

Je lance le binaire :

cd ..
./minetest/bin/minetestserver

Et je lance mon jeu depuis Minetest en spécifiant l’adresse IP du serveur. Tout se lance, génial. J’éteins le serveur, le plus complexe reste à venir.

Configuration du serveur PostgreSQL

Je relance par sécurité le service :

sudo systemctl restart postgresql.service

Et puis je lance mon interface PostgreSQL

sudo -u postgres psql 
CREATE USER monuser WITH PASSWORD '*****';
CREATE DATABASE adventure OWNER monuser;
GRANT ALL PRIVILEGES ON DATABASE adventure TO monuser;

Je vérifie avec /l que tout est ok et je sors

La migration de l’ancien serveur

Là, je suis obligée de dire à mes joueurs que le serveur doit être fermé pour la migration. J’éteins mon ancien serveur et je fais une copie de mon monde vers le nouveau. Je ne cache pas que ça prend du temps, vu le nombre de gigaoctets de mon map.sqlite.

Pour vous situer, il faut copier deux répertoires :
– celui des mods : .minetest/mods vers minetest/mods
– celui du monde, dans mon cas : .minetest/worlds/adventure vers minetest/worlds/adventure

Je tremble un peu durant ce transfert et prie pour que tout se passe bien. Je relance après cette étape en spécifiant mon monde et … ça se lance ! Tout va bien, je souffle un peu ! Maintenant passons aux choses sérieuses !

J’édite mon fichier world.mt du monde adventure pour y ajouter mes identifiants de connexion PostgreSQL. On ne touche à rien d’autre, notamment la configuration des backends en SQLite. Ce sera fait automatiquement.

pgsql_connection = host=127.0.0.1 port=5432 user=monuser password=***** dbname=adventure
pgsql_auth_connection = host=127.0.0.1 port=5432 user=monuser password=***** dbname=adventure
pgsql_player_connection = host=127.0.0.1 port=5432 user=monuser password=***** dbname=adventure

Et je lance ma première migration, celle du monde, et j’attends. Ce qui est sympa, c’est que l’avancée de la migration s’affiche :

./minetest/bin/minetestserver --migrate postgresql --world minetest/worlds/adventure

Si vous avez comme moi une base de données énorme, ne faites pas l’erreur que j’ai faite, de lancer le script dans un terminal et le laisser juste agir comme ça (sincèrement, je ne pensais pas que ça prendrait tant de temps !). Je suis partie me coucher, le lendemain mon ordi avait rebooté tout seul, interrompant le process (que j’avais lancé la veille à 15h30), et que j’ai dû relancer cette fois dans un screen (ou faites un nohup … &, bref, permettez au script de le laisser exécuté même si il y a une coupure.). Bref, une bonne vingtaine d’heures plus tard (oui, une VINGTAINE d’heures !!!) après le deuxième lancement, la base des blocs est migrée !

Regardons le fichier world.mt. Il a changé automatiquement le backend correspondant à la base des blocs. Nickel. Faisons la suite. Les autres bases de données migrent très vite.

./minetest/bin/minetestserver --migrate-auth postgresql --world minetest/worlds/adventure
./minetest/bin/minetestserver --migrate-players postgresql --world minetest/worlds/adventure

L’heure de vérité à sonné. Je vérifie que mes backends ont bien été modifiés et que tout pointe sur ma base PostgreSQL. Mieux que ça, je convertis mes fichiers map.sqlite, auth.sqlite et players.sqlite en map.old, auth.old et players.old. Je lance le jeu, et super, tout marche et je retrouve mon monde tel que je l’avais laissé …

Reste à remettre dans mon minetest.conf les paramètres de l’ancien serveur, sauf l’adresse, qui change.

Pour terminer, augmentez la mémoire allouée pour les requêtes PostgreSQL, sinon vous allez vous retrouver avec des plantages core dumped. Donc dans /etc/postgresql/potgresql.conf, changez la valeur du paramètre shared_buffer pour qu’il ait au moins une valeur supérieure à 512Mo. Chez moi, j’ai affecté 1024Mo.

Il ne reste plus qu’à profiter du jeu maintenant !

Mes notes du Symfony Live – Jour 1

Quelle joie pour moi de revenir à cette vie d’avant qui m’avait tant manquée … Il s’agit ici du deuxième Symfony Live auquel j’assiste, et comme bien des séminaires, j’ai appris pas mal de choses, que j’ai notées, remises au propre, et vous fais partager !

La keynote de Fabien Potencier

Fabien est en forme pour cette nouvelle session ! Pour démarrer le show, il nous fait un retour d’expérience sur un bug qu’il a eu avec Twig. Après une mise à jour des tests des selectors css, il y a eu un problème de « match » avec une expression qui devait figurer dans le code, via preg_match(). Il commence alors une session de debug, pour avoir un détail plus explicite de la source du bug.
S’inclut alors un petit speech sur l’open source, qui consiste à contribuer pour :
– éviter aux autres ou à soi de refaire la même erreur
– éviter à quelqu’un, voire des centaines de gens de réfléchir de nouveau à un problème
Du coup, quick fix, on fork, on ouvre le code, on essaye de faire le changement, on écrit les tests, on les exécute, on pull request et on croise les doigts 🙂 …
Quand il s’agit d’un package, on peut aussi modifier le code et faire un link dans les vendors pour pouvoir tester plus efficacement.
Cependant, ici il s’agit d’une modification dans ExpressionLangage, qui est une sous-partie de Twig. On peut faire mieux que simplement corriger, ou faire une fonctionnalité qui peut poser problème avec d’autres cas de figure. Du coup, Fabien a poussé la réflexion plus loin … On est en PHP8.1 et on supporte PHP7 dans Twig, ce qui pose problème pour la contribution. La solution : travailler sur Twig4 pour moderniser le code. On ouvre les issues, entre autres : résolution des mismatches camelcase vs. snake_case, ne plus avoir de fonctions globales, analyse syntaxique …
En fin de compte : Twig sera intégré en tant que composant de Symfony, Twig4 sera alors nommé Twig6.2 en parallèle à la nouvelle version de Symfony, et changera de namespace.

Mon avis : une bonne mise en bouche, qui m'a poussée à la réflexion quand à certains de mes side projects.

Comment valider de la donnée – Marion Horteau

Ici, Marion nous fait un parallèle avec l’univers de Pokémon. Personnellement, je ne suis pas du tout fan, mais je me suis laissée tenter.
Pour rappel, un validateur est composé d’une contrainte, qui est appliquée à un objet, et elle est appliquée à un validateur. nativement, il y en a une bonne dizaine (que j’ai du bûcher par cœur pour ma certif …).
Mais certaines contraintes ne sont pas forcément liées à un validateur, comme UniqueEntity dans Doctrine.
On valide sur une propriété, dans un formulaire, dans une méthode (getter ou autre), ou sur une classe entière, et soit dans le code, soit lors d’un submit dans un formulaire.
Mais ici on veut faire de la validation dynamique …
Marion propose donc une solution basée sur une API avec des paramètres qui varient.
Dans le contrôleur, on alimente un objet Evolution, avec un attribut Callback. Comme les contraintes sont différentes, on peut en ajouter avec Assert\Collection. InContext permet de rendre la validation récursive.
Pour externaliser les contraintes, elle crée une interface pour les différents types de contraintes. Elle ajoute des tags dans la définition des services. La persistance se fait soit par Doctrine, soit par Json.

Mon avis : dommage que le contexte Pokémon me rebute, mais il s'agit ici d'un avis personnel. L'introduction sur les validateurs natifs est toutefois un peu longuette, j'aurais aimé qu'elle détaille plus en détails cette validation dynamique. Mais intéressant.

Du DDD avec API Platform – Mathias Arlaud, Robin Chalas

Rappelons-le, ce pattern permet une conception tactique et architecturale. le DDD ce n’est pas du RAD, la conception est pilotée par le métier, et ça ne permet pas de développer rapidement.
Petit rappel de l’archi hexagonale : les layers sont des couches où on écrit du code, ils obéissent à la règle des dépendances.
– Couche basse : domaine (models, events, repo, services …)
– Couche application : contient les services applicatifs pour traiter les use-case métier
– Couche infrastructure : porte vers le monde extérieur (controllers, bases de données, caches, vendors …), c’est la SEULE qui aura accès à du code tiers.
Les avantages :
– on préserve l’intégrité de notre domaine,
– le code est d’avantage testable,
– on est capable de repousser les décisions technologiques,
– le domaine est agnostique du contexte utilisé (ligne de commande, terminal, navigateur …)
Dans Api Platform, on peut configurer via ApiResource les opérations, via new Get() et new Post(). Mais ce n’est pas la bonne approche.
Le pattern command bus va nous permettre de reprendre complètement le main sur nos use case métier en faisant appel à nos domaines. Ça implique : Notion de commande (objet PHP) + un handler (service applicatif) + bus (trouver le bon handler pour une commande données) + deuxième bus (query) pour nous permettre de contrôler la manière dont on interagit avec notre domaine.
On manipule entre autres :
ItemOperations
CollectionOperations (ça dépend si on a un id dans le chemin) – supprimée dans ApiP3, tout est operation
– Notion de ChainProvide qui fait appel aux autres providers
– On peut créer nos propres providers, qui auront d’autres priorités
– Création de DataTransformers

Mon avis :  Haaa ! API Platform ! J'adooore ce framework, et je voulais vraiment en savoir plus sur le Domain Design Development. Je comprends ici que je n'ai pas fait le tour de ce framework, je pense à certains side projects que je définis en DDD, qui font appel à une API crée par ApiP, mais j'ai envie de remanier tout ça. Conférence très interessante qui m'a remise en question. Merci :D !

Doctrine, objet typé, et colonne JSON – Grégoire Pineau

Ici, on parle CMS, notamment celui utilisé par JoliCode. Composé de blocs, qui représentent chacun un objet.
L’objectif de ces blocs :
– éviter la duplication de code
– pouvoir stocker les blocs en BDD
– avoir un maximum d’objets PHP
– et si possible, un objet par type de bloc
Pour ce faire, deux solutions : utiliser l’héritage Doctrine, et stocker du PHP sérialisé.
Les options :
MappedSuperClass : Doctrine ne sait pas gérer les relations avec
Single Table (genre une première classe) avec une entité. On aura une entité mais la deuxième classe sera vide, si la première est remplie.
Class Table : crée une table par type de bloc, pas très pratique à administrer.
On rejette d’emblée le PHP sérialisé via serialize() et unserialize(), non interopérable ! Quand à l’objet stocké en JSON, c’est « so 2013 » …
La solution proposée c’est le Concept Unit Of Work, la donnée est encapsulée via l’EntityManger qui :
– contient le cache de toutes les entitées passées
– gère les écritures en BDD,
– gère les transactions,
– s’occupe de quel objet doit être insert / delete / update
– s’occupe de gérer les dépendances entre les objets
– fait des snapshots des objets qu’il rencontre pour faire un diff au moment du flush.
On utilise alors les event Doctrine, qu’on hooke dans notre code, avec un listener Doctrine.

Mon avis : Je vais me faire taper sur les doigts, mais je vais être franche. C'est assez pénible quand les slides sont passées vite-vite quand on ne connait pas vraiment le sujet (qui ici est spécifique, puisque retour d'expérience). J'ai eu beaucoup de mal à suivre. Personnellement, j'aurais préféré moins de contenu, mais peut-être plus d'explications.

Connaissez-vous vraiment JWT ? – Karim Pinchon

Pour rappel, JWT (prononcé abusivement « jot ») = Json Web Token.
C’est une solution simple et sécurisée, qui a une référence et une valeur.
La cryto, ça obéit à des régles : c’est mathématique, c’est de confiance, ça communique, ça passe sur un canal non sûr, et c’est CAIN.
ça implique :
– la signature numérique
– le chiffrement
– le hachage (intégrité et sens unique)
– Mac / HMac (intégrité et authentification)
– encodage (chaine de caractères)
Pour voir à quoi ça ressemble, il existe les outils token.dev et jwt.io
Après un tour des tokens existants (JWE, JOSE …), quelques conseils :
– attention à ne pas tout accepter côté serveur
– valider les claims
– préférer l’asymétrique
– utiliser une librairie existante et éprouvée
– ne vous battez pas pour révoquer les JWT (ça doit avoir un cycle de vie)
– éviter de logger les jetons (pour la confidentialité)

Mon avis : je suis peu familière avec le cryptage des données, car je n'en ai fait que rarement dans ma carrière, mais c'est bon à savoir.

Des composants Symfony méconnus qui valent le détour – Alexandre Daubois

  • HTMLSanitizer : prévu pour SF 6.1, expérimental.
    HTMLPurifier : garde au mieux l’arborescence des noeuds, supprime les données potentiellement dangereuses.
    Le Sanitizer reconstruit les données du HTML en extrayant les données safe de l’input, la structure de base peut être perdue. Le comportement est défini par HTMLSanitizerConfig. Il peut forcer les https pour les URL, autorise les Schemes, autorise les media hosts.
  • STRING : arrivé en SF 5.0, il permet de faire de la manipulation simplifiée de codepoints, par exemple des emojis : combinaisons possibles pour faire des couleurs de peau, crée des chaines par contructeur (CodePointString, UnicodeString, ByteCodeString …), crée des chaines par factories.
  • OptionsResolver (remplace SPL) : valeurs obligatoires, callbacks, etc … , normalisation pour résoudre les options.
  • L’internationalisation : ce n’est pas seulement de la traduction (dates, nombres, monnaies, etc …), INTL depuis SF 2.3 (mai 2013).
    International Component For Unicode : quelques entreprises bien connues l’utilisent. Permet de formater : les langues, les locales, les monnaies, les timezones (reprises par les forms, qui vont puiser dans ce composant.)
Mon avis : je connaissais déjà OptionsResolver, mais c'est bien sympa d'en connaitre d'autres ! :D ...

Un fabuleux mapping avec Leaflet.js !

Aujourd’hui, pour changer, je vous parlerai de … front-end ! Bien que plus dévolue back, j’avais envie de faire d’une pierre deux-coups et de réaliser un joli mapping de mon monde Minetest.

Avec Minetest, c’est assez facile de générer des fichiers image qui retracent en 2D l’univers généré dans le jeu, avec minetestmapper. A partir de là, les plus créatifs peuvent s’amuser.

Pour la petite histoire, je suis en fait tombée sur le plan interactif du serveur Minetest LinuxForks, qui est juste splendide, et j’ai eu vraiment envie de me lancer ce défi.

Au commencement, une map qui rame à mort !

Du coup, j’ai intégré ce fichier dans une carte de base. Le code est simple, j’intègre Leaflet.js dans ma page web, et j’y intègre quelques markers. La bibliothèque, je la connaissais déjà, car je l’avais déjà intégrée pour l’outil d’un de mes clients BTP, pour y localiser les chantiers en cours. Mais Leaflet, c’est vraiment l’outil qui permet de créer des cartes, sans forcément qu’elle soit « terrestre », une « mappemonde ». Donc pas mal privilégiée par les geeks gamers.
Voilà grosso modo le code de base du premier jet que j’ai intégré dans mon index (j’y ai intégré après un div vide appelé map, bien entendu):

var map = L.map('map', {
    crs: L.CRS.Simple,
    center: [500, 500],
    scale: function (zoom) {
        return Math.pow(2, zoom);
    },
    zoom: function (scale) {
        return Math.log(scale / 256) / Math.LN2;
    },
});
 
var bounds = [[0,0], [2000,2000]];
var image = L.imageOverlay('map.png', bounds).addTo(map);
// Spawn
var spawn = L.latLng([971.5, 1156.5]);
L.marker(spawn).addTo(map).bindTooltip('Spawn Station', {sticky: false, direction: 'top'});
// Maison de Jym
var jymHome = L.latLng([972, 1137]);
L.marker(jymHome).addTo(map).bindTooltip('Maison de Jym', {sticky: false, direction: 'top'});
 
map.setView( [971.5, 1156.5], 3);

C’est déjà pas mal. La définition de la map en crs permet de générer une map personnalisable, le niveau de zoom est correct, on peut naviguer pépère. Sauf que la carte, qui est grande, met pas mal de secondes à charger, c’est lourd, très lourd. Et ça, c’est un gros souci. Sans compter que la map devient floue à un certain niveau de zoom avancé. A revoir, donc !

Le tiling, solution miracle !

Et puis j’apprends que Leaflet intègre très bien des briques d’images. A la base, on peut sans souci utiliser un coup d’imagemagick pour ce faire et recoller les morceaux, chose que la bibliothèque fait très bien. Donc pile ce dont j’ai besoin !

Pour créer les briques, il y a un script existant qui gère Leaflet. Il est en Python et s’appelle gdal2tiles-leaflet. Ne pas oublier avant de faire un petit sudo apt install python-gdal (pour les utilisateurs d’Ubuntu) et éventuellement installer python si ce n’est déjà fait, juste avant de l’utiliser.

Voici la commande que j’ai lancée :
./gdal2tiles.py -l -p raster -z 0-10 -w none ../map.png ../tiles
Et j’ai attendu quelques bonnes minutes pour que toutes les briques soient générées. C’est un peu long mais ça vaut le coup. Ne pas oublier l’option -p raster car on en aura besoin par la suite, et surtout l’option -l qui définit spécialement des briques adaptées pour leaflet. J’ai demandé un niveau de zoom variable de 0 à 10, pour bien pouvoir avoir chaque détail de ma carte.

Gérer le tiling dans Leaflet

Maintenant qu’on a généré les briques, il faut pouvoir les assembler. Là encore, après pas mal de recherches, j’ai pu trouver de quoi satisfaire ma faim.

Bien avant, il faut intégrer la librairie rastercoords, pour pouvoir faire la correspondance des briques au format « raster » (l’option que nous avions vu plus haut). Et ainsi, avec ce code, vous pouvez déjà disposer d’une map sympa :

;(function (window) {
    function init (mapid) {
        var minZoom = 0
        var maxZoom = 9
        var img = [
            38192, // original width of image
            29792  // original height of image
       ]
    // create the map
    var map = L.map(mapid, {
       minZoom: minZoom,
       maxZoom: maxZoom
    })
 
    var rc = new L.RasterCoords(map, img)
    map.setView(rc.unproject([22000, 15450]), 9)
    L.control.layers({}, {
        //@todo
    }).addTo(map)
 
    L.tileLayer('./tiles/{z}/{x}/{y}.png', {
        noWrap: true,
        attribution: 'Creation Amelie DUVERNET aka Amelaye <a href="http://minetest.amelieonline.net">Projet Amelaye In Minerland</a>'
        }).addTo(map)
    }
 
    init('map')
}(window))

Des markers jolis et dynamiques

Nous avons la carte, il faut maintenant y ajouter les markers. Pour ce faire, vous avez besoin de :
– La librairie font-awesome (on en a besoin pour extra-markers)
– La librairie leaflet-extra-markers

J’ai crée deux fonctions, la première, layerGeo qui permet de récupérer les points définis dans mon fichier geojson.js

function layerGeo (map, rc) {
  var layerGeo = L.geoJson(window.geoInfo, {
    // correctly map the geojson coordinates on the image
    coordsToLatLng: function (coords) {
      return rc.unproject(coords)
    },
    // add a popup content to the marker
    onEachFeature: function (feature, layer) {
      if (feature.properties &amp;&amp; feature.properties.name) {
        layer.bindPopup(feature.properties.name)
      }
    },
    pointToLayer: function (feature, latlng) {
      return L.marker(latlng, {
        icon: feature.properties.id
      })
    }
  })
  map.addLayer(layerGeo)
  return layerGeo
}

Ainsi qu’une autre, qui permet de trouver les « frontières » de la carte, et également d’afficher les coordonnées d’un emplacement au hasard cliqué :

function layerBounds (map, rc, img) {
  // set marker at the image bound edges
  var layerBounds = L.layerGroup([
    L.marker(rc.unproject([0, 0])).bindPopup('[0,0]'),
    L.marker(rc.unproject(img)).bindPopup(JSON.stringify(img))
  ])
  map.addLayer(layerBounds)
 
  // set markers on click events in the map
  map.on('click', function (event) {
    // to obtain raster coordinates from the map use `project`
    var coord = rc.project(event.latlng)
    // to set a marker, ... in raster coordinates in the map use `unproject`
    var marker = L.marker(rc.unproject(coord)).addTo(layerBounds)
    marker.bindPopup('[' + Math.floor(coord.x) + ',' + Math.floor(coord.y) + ']').openPopup()
  })
  return layerBounds
}

Du coup, on revient sur notre code qui affiche la map et on corrige la ligne L.control.layers pour appeler nos fonctions :

L.control.layers({}, {
  'Bounds': layerBounds(map, rc, img),
  'Info': layerGeo(map, rc)
}).addTo(map)

Maintenant passons aux choses sérieuses, la définition de nos coordonnées. Et là, on va passer nos jolis markers, ainsi que les points qui nous intéressent, et tout ceci va se passer au niveau d’un nouveau fichier geojson.js :

;(function (window) {
    // Markers
    var spawnStation = L.ExtraMarkers.icon({
        icon: 'fa-anchor',
        markerColor: 'red',
        shape: 'star',
        prefix: 'fa'
    });
 
    var metroStation = L.ExtraMarkers.icon({
        icon: 'fa-subway',
        markerColor: 'blue',
        shape: 'circle',
        prefix: 'fa'
    });
 
    var castle = L.ExtraMarkers.icon({
        icon: 'fa-dungeon',
        markerColor: 'violet',
        shape: 'square',
        prefix: 'fa'
    });
 
    // etc ...
 
    // geoJson definitions
    window.geoInfo = [
        {
            'type': 'Feature',
            'properties': {
                'name': 'Spawn Station',
                'id': spawnStation
            },
            'geometry': {
                'type': 'Point',
                'coordinates': [22087, 15321]
            }
        },
        // Castles
        {
            'type': 'Feature',
            'properties': {
                'name': 'Chateau dans le ciel',
                'id': castle
            },
            'geometry': {
                'type': 'Point',
                'coordinates': [22088,14112]
            }
        },
        {
            'type': 'Feature',
            'properties': {
                'name': 'Chateau Royal',
                'id': castle
            },
            'geometry': {
                'type': 'Point',
                'coordinates': [22229,15299]
            }
        },
        {
            'type': 'Feature',
            'properties': {
                'name': 'Chateau Amelaye',
                'id': castle
            },
            'geometry': {
                'type': 'Point',
                'coordinates': [22060,15402]
            }
        }, // etc ...
    ]
}(window))

Là, j’ai pas mal galéré car il faut injecter les markers personnalisés dans un fichier qui doit être scrupuleusement rigoureux, car geojson est une norme. J’ai donc passé les variables qui correspondent à la définition des « templates de markers » dans la propriété id. C’est caduque, bricolé, mais ça marche.

Et voilà le résultat (disponible en ligne) :

Nous voici maintenant avec une map rapide à charger, qui intègre pas mal de points personnalisables. Il reste pas mal de choses à faire, j’ai bien envie d’utiliser une base de données pour créer une API, voire une base ElasticSearch qui se chargera de générer le fichier geojson, mais pour l’instant je n’ai pas décidé de la suite. Qu’en pensez-vous ?

Voici pour plus de détails le rendu final sur mon Github.

Comment monter un serveur d’archives PHP sans pleurer …

Mine de rien, concernant mon métier j’ai fait mes premiers pas en commençant mes études, ce qui commence à faire … un petit paquet d’années. Du coup en fait j’ai gardé (presque) toutes mes archives … ce qui n’est pas problématique quand on est en « full HTML » mais quand on passe en PHP, là c’est plus touchy. Parce que mes premiers sites sont en PHP4, en sachant qu’on se dirige vers la version 8, il y a eu pas mal de chemin depuis : renforcement de la rigueur, changements dans la conception objet, méthodes deprecated et j’en passe.

Essayez de faire marcher un site crée en 2003 sur un PHP 7 … ça m’étonnerait que vous puissiez faire quelque chose de concret. Du coup, pour faire comme moi, trouver des trucs de geek à faire en étant confiné (même si j’ai la chance de pouvoir continuer à travailler, il faut s’occuper le soir 😉 ) et faire tourner un serveur d’archives sur une Ubuntu récente, que faire ?

PHP 5.6 avec FPM

Il y a une solution pour les sites les plus récents : PHP-FPM, qui permet de faire tourner PHP en service indépendamment d’Apache (oui désolée je suis Apache-addict, question d’habitude), ce qui vous permet de pouvoir faire tourner un PHP5.6. C’est assez simple …

En premier lieu, on prépare le terrain, on se base déjà sur le fait que vous avec déjà Apache installé dans votre système.

Quelques librairies de base

Puis il vous faut ajouter le fameux repository d’Ondřej Surý, qui a fait un travail remarquable à ce sujet :

Et hop, magie !

N’oubliez pas d’ajouter les librairies MySQL au besoin. Une fois les installations bien terminées, faites un petit service status, histoire de bien vérifier que les services tournent :

Donc si vous avez un message de ce style, c’est que tout va bien 🙂

Donc là, vous pouvez faire tourner non seulement des sites tout neufs, mais également des sites que vous avez conçu il y a au moins trois-quatre ans. Il suffit après de configurer votre vhost comme suit pour switcher de version :

Vérifiez bien que les services suivants soient activés :
actions proxy_fcgi alias
Et les librairies suivantes installées pour PHP 5.6 au risque de voir une belle page blanche qui ne logge rien de pertinent :
php5.6-xml php5.6-gd php5.6-mcrypt php5.6-mysql php5.6-pdo

Et pour les versions antérieures à PHP 5.6 ?

Après, les choses se compliquent. Ne cherchez pas à installer ne serait-ce que PHP 5.2 à la mano, rien n’est compatible avec les versions actuelles d’OpenSSL et vous risquez de casser pas mal de trucs comme ça m’est arrivé. Et ainsi d’avoir à vous repalucher les installs d’Apache et OpenSSL, les joies de l’admin sys et de l’expérimentation, en somme 🙂 … – note : ce serveur n’est pas celui que j’utilise pour mes sites importants mais pour mon « bac à sable » 😉 …

Du coup j’ai opté pour un bel outil bien tendance qui a le vent en poupe chez les devOps, et qui est vraiment pratique, j’ai nommé : Docker ! J’ai finallement consenti à créer donc un conteneur qui ne servira qu’à mes plus anciens sites, et la bonne nouvelle, c’est qu’il n’y a pas tant de différences entre les dernières versions de PHP 4 et PHP 5.2 (au niveau du fait de pouvoir faire fonctionner un site en PHP 4 sur un serveur PHP 5.2, l’inverse ne sera pas forcément possible – et même fortement impossible), ce qui signifie que mes plus vieux sites tournent avec sans souci, et que je n’ai donc pas à créer de conteneurs supplémentaires (du moins, pour ma part ! ) !

Comme je suis une nana sympa, j’ai mis sur GitHub l’image que j’utilise, que vous pouvez déployer avec docker-compose, vous pouvez même définir un répertoire mysqldump pour alimenter la base directement en ligne de commande. Il y a également un PhpMyAdmin pour se simplifier la vie. Il faut juste modifier les vhosts pour que ça pointe sur vos sites à vous, qui seront accessible par défaut sur le port 8052 (80 port par défaut d’Apache et 52 comme PHP 5.2 😉 )

N’oubliez pas de préciser au besoin comme dans mon exemple, si vos extensions sont .php3, je sais ça fait super bizarre de retrouver ce genre d’extension …

Pour le plaisir, je vous montre une petite capture d’écran de ma page de garde, où je peux naviguer de site en site … c’est amusant de replonger dans le passé, comme ça :

Après, c’est un projet qui donne plein d’idées : par exemple vous voyez les petits screenshots ? Ils sont en réalité bien moches quand on passe en responsive, car dimensionnés assez petits (je pensais qu’à l’époque, ça suffisait). Bien là j’ai mis en place un process sous Puppeteer qui permet de faire des captures d’écran automatiquement ! Je pense que je vais pas mal trouver d’idées inspirantes que je vous partagerai par la suite 😉

Mon projet de refacto BioPHP – Partie 2 (du design pattern)

Le souci de la librairie d’origine de BioPHP, c’est son absence de design pattern. Le code d’origine prévoyait d’inclure deux systèmes de bases de données (Genbank et Swissprot en l’occurence) voire d’autres, car j’ai vu une dizaine d’autres formats.

Or le but du jeu c’est d’avoir une structure assez carrée pour pouvoir ensuite manipuler les données.

Tout se joue dans le fichier d’origine seqdb.php, je vous laisse regarder la tambouille. Dans ce cas-là, il faut surtout comprendre la logique, la garder et tout casser en même temps. A coups de massue.

La première des choses que j’ai faite, c’est compartimenter une classe par fichier, et un fichier par classe : donc on a trois fichiers, SeqDB, et des fonctions parse_swissprot et parse_id que je ramène dans une classe chacune, et que je renomme au passage car sinon on n’y comprend pas grand-chose. Je passe l’étape de « nettoyage », remplacement des fonctionnalités obsolètes, découpage du code en méthodes privées, simple, basique.

Deuxièmement, c’est d’utiliser un Iterator pour le flux des fichiers car le script réinvente la roue avec les next() et les prev().

Ensuite j’ai refactorisé en gardant l’ancienne structure de seq.php, pour arriver à comprendre petit à petit la logique, à quoi correspond chaque champ du fichier qui est envoyé. Avant le grand coup d’envoi, puisque les structures qui correspondent aux séquences vont être factorisées à leur tour, pour cela je me suis inspirée du modèle de données qui avait été proposé par l’équipe, en y mettant deux trois ajustements :

Rien de bien sorcier, c’est du MVC et du Doctrine de base.

Une fois que c’est établi, que ça marche et parse, il faut intégrer ce système au parsing des fichiers tout en permettant à l’utilisateur de lui simplifier un maximum le travail.

Il fera appel à une interface. Une seule. Et la librairie se chargera du reste. Voici comment (le travail n’est pas terminé ce qui explique pourquoi ParseSwissProt est vide mais c’est en cours) :

Diagramme réalisé avec le superbe outil PlantUML

Utiliser l’interface DatabaseInterface fera appel via l’injection de dépendances à DatabaseManager qui se charge de vérifier si le fichier existe en base, ou d’aller chercher de la donnée. Suivant le cas de figure, ce sont des factory qui seront appelées et qui joueront les aiguilleurs en appelant les services correspondants. Ces services « final class » ont une notion d’héritage sur un autre service, abstrait, ParseDbAbstractManager, qui contient l’architecture de la donnée qui sera renvoyée dans le controller de l’utilisateur.

Ce qui permet dans tous les cas de renvoyer une donnée, qui, quel que soit le schéma de départ, sera uniforme. Et ce grâce à deux design pattern : Factory et Abstract.

Du coup, si on veut ajouter une autre structure de fichier, il suffira de modifier les factory et d’ajouter un service qui sera chargé uniquement de parser la donnée. Ceci dit, je pense qu’il est même possible de passer outre la modification des factory, je dois réfléchir sur ce point.

Je crois que cette partie est la plus complexe de mon projet, car je ne suis pas familiarisée avec les bases de données textuelles propres à la bioinformatique, et le script de départ contenait pas mal de bugs. Du coup si des biologistes chevronnés constatent des bugs à l’utilisation, ils peuvent me contacter, ce sera volontiers que je procèderai aux ajustements.