Ajouter une galerie d’images à un éditeur de texte enrichi en Node.js

La boîte de dialogue Insert Gallery de RichTextEditor peut afficher les images déjà présentes sur votre serveur : les utilisateurs choisissent ainsi une image existante au lieu de l’envoyer à nouveau. La boîte de dialogue a besoin d’une URL qui répond avec le contenu d’un dossier en JSON. Cette page construit ce point de terminaison avec Express 5, uniquement avec node:fs, et y relie la boîte de dialogue.
Diriger la boîte de dialogue vers le serveur
Une seule option de l’éditeur fait passer la galerie de ses images d’exemple intégrées à votre serveur :
<div id="editor"><p>Hello from Express.</p></div>
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "/rte/gallery",
maxUploadFileSize: 4000000
});
</script>Avec galleryEndpoint défini, la boîte de dialogue envoie GET /rte/gallery?folder= à son ouverture, puis de nouveau avec ?folder=blog quand quelqu’un ouvre un dossier. Le bouton Upload de la boîte de dialogue nécessite rte-files.js, présenté dans le guide sur l’envoi. Sans lui, la boîte de dialogue ne peut que parcourir.
Ce que renvoie le point de terminaison
La liste indique le dossier courant, son parent, les sous-dossiers et les images. Chaque image a une URL que l’éditeur peut insérer, un nom affiché, son dossier et sa taille en octets. Voici une réponse réelle de l’exemple :

parentFolder vaut null à la racine. C’est ainsi que la boîte de dialogue sait qu’elle doit désactiver son bouton Up. Les dossiers sont des chemins virtuels séparés par des /, comme blog/2026, jamais des chemins sur le disque.
La route et la liste
La route Express tient en une ligne. Express 5 transmet au middleware d’erreurs les erreurs du gestionnaire async :
// Every /rte request gets its storage root first, before multer reads the body.
app.use("/rte", (req, res, next) => {
req.storageRoot = rootFor(req);
next();
});
// GET /rte/gallery?folder=products -> listing
app.get("/rte/gallery", async (req, res) => {
res.json(await listFolder(req.storageRoot, req.query.folder ?? ""));
});
// POST /rte/gallery action=create-folder, folder, name -> listing of the parent folder
app.post("/rte/gallery", parseForm.none(), async (req, res) => {
const { action, folder, name } = req.body ?? {};
if (action !== "create-folder") throw new GalleryError(400, "Unknown action.");
res.json(await createFolder(req.storageRoot, folder ?? "", name));
});
// POST /rte/upload file, folder -> { url }
app.post("/rte/upload", parseForm.single("file"), async (req, res) => {
if (!req.file) throw new GalleryError(400, "No file was uploaded.");
const url = await saveImage(req.storageRoot, req.body.folder ?? "", req.file.originalname, req.file.buffer);
res.json({ url });
});listFolder(), dans gallery.mjs, lit le dossier avec readdir(dir, { withFileTypes: true }) et construit le JSON :
// The JSON the gallery dialog reads (see CONTRACT.md).
export async function listFolder(root, folder = "") {
if (isUsersFolder(root, folder)) throw new GalleryError(404, "Folder not found.");
const dir = await resolveFolder(root, folder);
const entries = await readdir(dir, { withFileTypes: true });
const join = (name) => (folder ? folder + "/" + name : name);
const folders = entries
.filter((entry) => entry.isDirectory() && isValidName(entry.name) && !isUsersFolder(root, join(entry.name)))
.map((entry) => ({ folder: join(entry.name), name: entry.name }));
const images = [];
for (const entry of entries) {
if (!entry.isFile() || !isImageName(entry.name)) continue;
const file = path.join(dir, entry.name);
const { size } = await lstat(file);
images.push({ url: publicUrl(file), name: entry.name, folder, size });
}
return {
currentFolder: folder,
currentFolderDisplay: "/" + folder,
parentFolder: folder === "" ? null : folder.split("/").slice(0, -1).join("/"),
folders: folders.sort(byName),
images: images.sort(byName),
};
}
// Plain lower-case comparison, so every server sample sorts the same way.
function byName(a, b) {
const x = a.name.toLowerCase();
const y = b.name.toLowerCase();
return x < y ? -1 : x > y ? 1 : 0;
}Quelques choix y ont leur importance :
- Seuls les fichiers ayant une extension d’image sont listés, et rien de ce qui commence par un point.
- Seuls les dossiers ayant un nom valide sont listés : chaque dossier affiché par la boîte de dialogue peut donc aussi être ouvert.
- Le tri est une simple comparaison en minuscules plutôt que
localeCompare. L’ordre est le même sur chaque machine et dans chaque langage dans lequel les exemples sont écrits. - Les URL sont construites à partir du chemin sous
uploads/, chaque partie passant parencodeURIComponent. Un dossier nomméMy Photosdonne/uploads/My%20Photos/....
Avant tout cela, resolveFolder() transforme ?folder= en chemin sur le disque et refuse tout ce qui sortirait de uploads/. Dossiers de galerie en Node.js détaille cette vérification.
Une chaîne de requête peut être un tableau
Express analyse ?folder=a&folder=b en ["a", "b"]. C’est vrai dans Express 4 comme dans Express 5. Ce qui a changé dans la version 5, c’est la syntaxe imbriquée : lors de notre test, ?a[b]=1 a donné { a: { b: "1" } } avec Express 4.22 et { "a[b]": "1" } avec Express 5.3, car Express 5 utilise par défaut l’analyseur de requête simple. Dans les deux cas, req.query.folder n’est pas toujours une chaîne. resolveFolder() refuse avec un 400 tout ce qui n’est pas une chaîne : un tableau n’atteint donc jamais path.resolve.
Ce que la boîte de dialogue affiche, et ce qu’elle n’affiche pas
L’image en haut de la page montre la boîte de dialogue sur ce serveur : trois dossiers et quatre images à la racine, l’une d’elles sélectionnée. Deux limites sont bonnes à connaître :
- L’indication de la galerie dans la version publiée mentionne « JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF » comme formats acceptés. Ce serveur n’accepte que .jpg, .jpeg, .png, .gif et .webp. Un fichier .bmp choisi dans la boîte de dialogue est envoyé, refusé avec un 400, et la boîte de dialogue signale « Upload failed for <name>. ».
- Les vignettes sont les images complètes, chargées depuis
/uploads/. Un dossier contenant beaucoup de grandes photos les charge toutes. L’exemple ne crée pas de copies réduites.
Tester
Démarrez le projet d’exemple de ce guide avec npm install et npm start, envoyez une image dans un dossier, puis demandez la liste :
curl -F "file=@photo.jpg" -F "folder=blog" http://localhost:5101/rte/upload
{"url":"/uploads/blog/photo-ce99c014.jpg"}
curl "http://localhost:5101/rte/gallery?folder=blog"
{"currentFolder":"blog","currentFolderDisplay":"/blog","parentFolder":"","folders":[],"images":[...]}
curl "http://localhost:5101/rte/gallery?folder=../"
{"error":"Invalid folder."}La dernière réponse est un 400. Ouvrez ensuite http://localhost:5101, cliquez sur Insert Gallery dans la barre d’outils et ouvrez le dossier blog. L’image que vous avez envoyée figure dans la liste.
Le même guide pour d’autres plateformes
Next.js · PHP · ASP.NET Core · Razor Pages · Blazor · MVC 5 · Web Forms