Adicionar uma galeria de imagens a um editor de texto rico em Node.js

A caixa de diálogo Insert Gallery do RichTextEditor pode mostrar as imagens que já estão no seu servidor, para que as pessoas escolham uma imagem existente em vez de enviá-la de novo. A caixa de diálogo precisa de uma URL que responda com a listagem de uma pasta em JSON. Esta página cria esse endpoint no Express 5 usando apenas node:fs e conecta a caixa de diálogo a ele.
Apontar a caixa de diálogo para o servidor
Uma opção do editor faz a galeria trocar as imagens de exemplo embutidas pelo seu servidor:
<div id="editor"><p>Hello from Express.</p></div>
<script>
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "/rte/gallery",
maxUploadFileSize: 4000000
});
</script>Com galleryEndpoint definido, a caixa de diálogo envia GET /rte/gallery?folder= ao abrir e, de novo, com ?folder=blog quando alguém abre uma pasta. O botão Upload da caixa de diálogo precisa do rte-files.js do guia de upload. Sem ele, a caixa de diálogo só permite navegar.
O que o endpoint retorna
A listagem informa a pasta atual, a pasta pai, as subpastas e as imagens. Cada imagem tem uma URL que o editor pode inserir, um nome de exibição, sua pasta e seu tamanho em bytes. Esta é uma resposta real do exemplo:

parentFolder é null na raiz. É assim que a caixa de diálogo sabe que deve desativar o botão Up. As pastas são caminhos virtuais com separadores /, como blog/2026, nunca caminhos em disco.
A rota e a listagem
A rota do Express tem uma linha. O Express 5 repassa os erros do handler async para o middleware de erro:
// 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(), em gallery.mjs, lê a pasta com readdir(dir, { withFileTypes: true }) e monta o 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;
}Algumas escolhas nele são importantes:
- Somente arquivos com extensão de imagem são listados, e nada que comece com ponto.
- Somente pastas com nome válido são listadas, então toda pasta que a caixa de diálogo mostra também pode ser aberta.
- A ordenação é uma comparação simples em minúsculas, e não
localeCompare. A ordem é a mesma em qualquer máquina e em todas as linguagens em que os exemplos foram escritos. - As URLs são montadas a partir do caminho abaixo de
uploads/, com cada parte passada porencodeURIComponent. Uma pasta chamadaMy Photosgera/uploads/My%20Photos/....
Antes de tudo isso, resolveFolder() transforma ?folder= em um caminho em disco e recusa qualquer coisa que saia de uploads/. Pastas da galeria em Node.js explica essa verificação passo a passo.
Uma query string pode ser um array
O Express converte ?folder=a&folder=b em ["a", "b"]. Isso vale tanto no Express 4 quanto no Express 5. O que mudou no 5 foi a sintaxe aninhada: ?a[b]=1 gerou { a: { b: "1" } } no Express 4.22 e { "a[b]": "1" } no Express 5.3 no nosso teste, porque o Express 5 usa o parser de query simples por padrão. De qualquer forma, req.query.folder nem sempre é uma string. resolveFolder() recusa com um 400 tudo o que não for string, então um array nunca chega a path.resolve.
O que a caixa de diálogo mostra, e o que não mostra
A imagem no topo é a caixa de diálogo neste servidor: três pastas e quatro imagens na raiz, uma delas selecionada. Vale a pena conhecer duas limitações:
- A dica da galeria na versão lançada lista "JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF" como formatos aceitos. Este servidor aceita apenas .jpg, .jpeg, .png, .gif e .webp. Um arquivo .bmp escolhido na caixa de diálogo é enviado, recusado com um 400, e a caixa de diálogo informa "Upload failed for <name>."
- As miniaturas são as imagens completas, carregadas de
/uploads/. Uma pasta com muitas fotos grandes carrega todas elas. O exemplo não gera cópias menores.
Teste
Inicie o projeto de exemplo deste guia com npm install e npm start, envie uma imagem para uma pasta e depois peça a listagem:
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."}A última resposta é um 400. Depois, abra http://localhost:5101, clique em Insert Gallery na barra de ferramentas e abra a pasta blog. A imagem que você enviou está na lista.
O mesmo guia para outras plataformas
Next.js · PHP · ASP.NET Core · Razor Pages · Blazor · MVC 5 · Web Forms