Eine Bildergalerie für einen Rich-Text-Editor in Node.js

Der Dialog „Insert Gallery“ mit den Ordnern blog, products und team und vier Bildern aus dem Ordner uploads des Express-Beispiels, eines davon ausgewählt

Der Dialog „Insert Gallery“ von RichTextEditor kann die Bilder anzeigen, die bereits auf Ihrem Server liegen. So wählen Benutzer ein vorhandenes Bild aus, statt es erneut hochzuladen. Der Dialog braucht dafür eine URL, die mit einer Ordnerliste in JSON antwortet. Diese Seite baut diesen Endpoint in Express 5 nur mit node:fs und verbindet den Dialog damit.

Den Dialog auf den Server richten

Eine einzige Editor-Option schaltet die Galerie von ihren eingebauten Beispielbildern auf Ihren Server um:

HTML · public/index.html
<div id="editor"><p>Hello from Express.</p></div>
<script>
  var editor = new RichTextEditor("#editor", {
    toolbar: "full",
    galleryEndpoint: "/rte/gallery",
    maxUploadFileSize: 4000000
  });
</script>

Ist galleryEndpoint gesetzt, sendet der Dialog beim Öffnen GET /rte/gallery?folder= und erneut mit ?folder=blog, wenn jemand einen Ordner öffnet. Die Upload-Schaltfläche des Dialogs braucht rte-files.js aus der Anleitung zum Upload. Ohne diese Datei kann der Dialog nur durchsuchen.

Was der Endpoint zurückgibt

Die Liste nennt den aktuellen Ordner, seinen übergeordneten Ordner, die Unterordner und die Bilder. Jedes Bild hat eine URL, die der Editor einfügen kann, einen Anzeigenamen, seinen Ordner und seine Größe in Bytes. Das ist eine echte Antwort des Beispiels:

Das JSON, das GET /rte/gallery?folder=blog im Express-Beispiel zurückgibt: currentFolder blog, parentFolder leer, keine Unterordner und Bildeinträge mit url, name, folder und size
GET /rte/gallery?folder=blog, beantwortet vom Express-Beispiel.

parentFolder ist im Stammordner null. Daran erkennt der Dialog, dass er seine Schaltfläche für die nächsthöhere Ebene deaktivieren muss. Ordner sind virtuelle Pfade mit / als Trennzeichen, etwa blog/2026, niemals Pfade auf der Festplatte.

Die Route und die Liste

Die Express-Route ist eine einzige Zeile. Express 5 reicht Fehler aus dem async-Handler an die Fehler-Middleware weiter:

JavaScript · server.mjs
// 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() in gallery.mjs liest den Ordner mit readdir(dir, { withFileTypes: true }) und baut das JSON:

JavaScript · gallery.mjs
// 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;
}

Einige Entscheidungen darin sind wichtig:

  • Aufgeführt werden nur Dateien mit einer Bild-Dateiendung und nichts, was mit einem Punkt beginnt.
  • Aufgeführt werden nur Ordner mit einem gültigen Namen, sodass sich jeder Ordner, den der Dialog anzeigt, auch öffnen lässt.
  • Sortiert wird mit einem einfachen Vergleich in Kleinbuchstaben statt mit localeCompare. Die Reihenfolge ist auf jedem Rechner gleich und in jeder Sprache, in der die Beispiele geschrieben sind.
  • URLs werden aus dem Pfad unterhalb von uploads/ gebildet, wobei jeder Teil durch encodeURIComponent läuft. Ein Ordner namens My Photos ergibt /uploads/My%20Photos/....

Vor alldem wandelt resolveFolder() den Wert von ?folder= in einen Pfad auf der Festplatte um und lehnt alles ab, was uploads/ verlassen würde. Galerieordner in Node.js geht diese Prüfung Schritt für Schritt durch.

Ein Query-String kann ein Array sein

Express parst ?folder=a&folder=b zu ["a", "b"]. Das gilt für Express 4 und Express 5. Geändert hat sich in Version 5 die verschachtelte Syntax: ?a[b]=1 ergab in unserem Test unter Express 4.22 { a: { b: "1" } } und unter Express 5.3 { "a[b]": "1" }, weil Express 5 standardmäßig den einfachen Query-Parser verwendet. In beiden Fällen ist req.query.folder nicht immer ein String. resolveFolder() lehnt alles, was kein String ist, mit einem 400 ab, sodass ein Array nie bei path.resolve ankommt.

Was der Dialog anzeigt und was nicht

Das Bild oben zeigt den Dialog auf diesem Server: drei Ordner und vier Bilder im Stammordner, eines davon ausgewählt. Zwei Einschränkungen sollten Sie kennen:

  • Der Hinweis der ausgelieferten Galerie nennt „JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF“ als akzeptierte Formate. Dieser Server akzeptiert nur .jpg, .jpeg, .png, .gif und .webp. Eine im Dialog ausgewählte .bmp-Datei wird gesendet, mit einem 400 abgelehnt, und der Dialog meldet „Upload failed for <name>.“
  • Die Vorschaubilder sind die vollständigen Bilder, geladen aus /uploads/. Ein Ordner mit vielen großen Fotos lädt sie alle. Das Beispiel erzeugt keine verkleinerten Kopien.

Testen

Starten Sie das Beispielprojekt zu dieser Anleitung mit npm install und npm start, laden Sie ein Bild in einen Ordner hoch und fragen Sie dann die Liste ab:

Shell
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."}

Die letzte Antwort ist ein 400. Öffnen Sie dann http://localhost:5101, klicken Sie in der Symbolleiste auf Insert Gallery und öffnen Sie den Ordner blog. Das hochgeladene Bild steht in der Liste.

Dieselbe Anleitung für andere Plattformen

Next.js · PHP · ASP.NET Core · Razor Pages · Blazor · MVC 5 · Web Forms

Weiterlesen