DEV Community

Dev du Sud
Dev du Sud

Posted on

Un dossier, c'est une file d'attente : 21 articles publiés sans base de données

Je n'ai pas de back-office. J'ai un dossier.

Quand j'ai un article à écrire, j'ouvre un fichier, je le nomme 12-mon-sujet.md, je le pose
dans devto-articles/. Le lendemain matin, un job prend le premier fichier par ordre
alphabétique, le publie sur Dev.to, puis le déplace dans publies/. Vingt et un articles sont
passés par là. Aucun n'a été publié deux fois, aucun n'a été perdu, et je n'ai jamais eu à
ouvrir un tableau de bord pour savoir où j'en étais.

C'est l'architecture la plus banale qui existe — et celle que je conseille avant d'installer
n'importe quel outil de gestion de contenu.

Le nom du fichier est l'ordre de publication

Le job ne trie pas par date de modification, il ne lit pas un champ publish_at, il prend
sorted(os.listdir(...))[0]. Un seul critère, et il tient parce que le nom porte un numéro à
deux chiffres.

11-dossier-file-attente-devto.md
12-gmail-25-mo-sauvegarde.md
13-cron-3h-pc-eteint.md
Enter fullscreen mode Exit fullscreen mode

Le zéro de tête n'est pas décoratif. Sans lui, 10-... passe avant 9-..., et l'ordre de
publication devient le genre de bug qu'on ne voit qu'au bout d'un mois. Trois caractères, et le
problème n'existe pas.

Deuxième conséquence, plus intéressante : le nom est une promesse de calendrier. Un article
par jour, donc un numéro par jour. Si je pars trois jours, je pose trois fichiers et je sais
exactement ce qui sortira lundi, mardi, mercredi. Rien à planifier ailleurs.

Le filtre compte autant que le tri. Le dossier contient aussi des notes et des brouillons : la
sélection ne retient que les fichiers dont le nom commence par un chiffre.

CANDIDATS = sorted(
    p for p in RACINE.glob("*.md")
    if p.name[0].isdigit()           # ignore notes, brouillons, sauvegardes
)
fname = CANDIDATS[0] if CANDIDATS else None
Enter fullscreen mode Exit fullscreen mode

Un fichier = un article, et un seul

Un dossier, c'est une file d'attente ouverte : n'importe quoi peut y tomber. Chez moi, un
fichier de brouillons pour les réseaux sociaux s'y est retrouvé, et il a été publié avant que
je comprenne pourquoi. C'était un fichier markdown valide, avec un contenu, sans champ title.
L'API a créé l'article. Son titre était devenu « Sans titre ».

Depuis, la validation se fait avant le premier appel réseau, sur trois champs :

def valide(fm: dict) -> str | None:
    if not fm.get("title", "").strip():
        return "titre manquant"
    if not fm.get("description", "").strip():
        return "description manquante"
    if fm.get("published") != "false":
        return "published doit valoir false à la pose"
    return None
Enter fullscreen mode Exit fullscreen mode

Un fichier refusé reste dans le dossier, avec son nom intact. Il ne bloque pas la file : le job
le saute et prend le suivant. Un fichier invalide qui bloque toute la production, c'est une
panne ; un fichier invalide qui est sauté, c'est un avertissement.

Le published: false dans le front matter n'est pas une politesse. C'est la marque « cet
article attend son tour ». Le jour où je veux publier tout de suite, je passe par un PUT
manuel et je déplace le fichier à la main — mais le job, lui, ne publie jamais un fichier qui
s'est déjà déclaré publié ailleurs.

Le déplacement est la transaction

C'est le point sur lequel j'ai le plus réfléchi, parce que c'est le seul endroit où le système
peut perdre quelque chose.

L'ordre est strict, et il n'est pas négociable :

  1. l'API répond 2xx avec un id d'article ;
  2. le fichier est déplacé dans publies/ ;
  3. une ligne est ajoutée au journal.
draft = poster(article)                 # lève une exception si l'API refuse
shutil.move(src, DEST / src.name)       # rename atomique, même disque

with JOURNAL.open("a", encoding="utf-8") as f:
    f.write(f"{date}\t{src.name}\t{draft['id']}\t{draft['url']}\n")
Enter fullscreen mode Exit fullscreen mode

Si l'étape 1 échoue, rien n'a bougé. Si l'étape 2 échoue alors que l'article est en ligne, le
fichier repart le lendemain — et l'API répond 422 sur un titre déjà utilisé. C'est bruyant,
c'est réparable, et ça se voit. L'inverse — déplacer avant de publier — produit un article
perdu en silence. Entre les deux erreurs, j'ai choisi celle qui gueule.

Le shutil.move est sur le même disque, donc c'est un rename : atomique. Pas de fichier à
moitié copié, pas de doublon.

Ce qui se passe quand le dossier est vide

C'est le cas qui m'a coûté le plus cher, et il ne ressemble pas à une panne.

Zéro fichier candidat, zéro erreur. Le script sort en 0, le scheduler est content, et le
lendemain matin rien n'est en ligne. Aucun message, aucun log d'erreur, parce qu'il n'y a pas
d'erreur : il n'y a rien à faire.

Un job de publication qui n'a plus rien à publier est une alerte, pas un succès. Le script sort
maintenant en code 3, avec un message explicite :

if not todo:
    print("ALERTE: aucun fichier en attente")
    sys.exit(3)
Enter fullscreen mode Exit fullscreen mode

Et je garde une marge de sept fichiers d'avance. Vingt et un articles plus tard, la file n'est
jamais descendue en dessous de trois. Quand elle approche de zéro, c'est que j'ai arrêté
d'écrire — c'est l'information la plus utile que ce système me donne.

Le journal, trois colonnes

Chaque publication laisse une ligne, et cette ligne suffit :

2026-10-14  11-dossier-file-attente-devto.md  1874521  https://dev.to/...
Enter fullscreen mode Exit fullscreen mode

Je ne construis pas de statistiques dessus : tail -5 pour savoir ce qui est sorti cette
semaine, grep pour retrouver un article. Vingt et un articles, six cents octets de journal.

Pourquoi je n'ai pas ajouté de base de données

Parce que je n'en ai pas besoin, et parce que le système de fichiers fait déjà le travail :
lister, compter, trier, déplacer, horodater. Une table articles avec une colonne statut
aurait ajouté une deuxième source de vérité à synchroniser avec les fichiers. Deux sources de
vérité, c'est une divergence garantie et un script de réconciliation à écrire.

Ici, l'état est le dossier. Un article en attente est un fichier à la racine. Un article publié
est un fichier dans publies/. Rien à interroger, rien à réparer, et une sauvegarde qui tient
dans un tar.

La seule chose que j'ai ajoutée, c'est un verrou : flock autour du job, pour qu'un
déclenchement manuel et le cron ne prennent pas le même fichier à la même seconde.

Deux cent cinquante lignes de Python, un dossier, un fichier de journal. Vingt et un articles
sortis sans intervention, et un système que je peux encore lire dans trois ans sans relire de
documentation.

La leçon n'est pas « faites tout en bash ». C'est que la bonne file d'attente est souvent celle
qu'on a déjà sous la main, à condition d'accepter deux contraintes : un ordre explicite dans le
nom, et un déplacement qui sert de transaction. Le reste, c'est de la décoration.

Top comments (0)