Bilder aus einem Rich-Text-Editor in ASP.NET Core hochladen

Wenn jemand ein Bild hineinzieht, einfügt oder auswählt, sendet RichTextEditor es als multipart/form-data-POST an Ihren Server und setzt die zurückerhaltene URL in den Text ein. In ASP.NET Core ist das eine Action oder ein Minimal-API-Endpoint, der ein IFormFile empfängt. Diese Seite zeigt beides, was der Server prüft, bevor er etwas speichert, und die Antworten, die Sie mit curl testen können.
Die Seite
Der Editor wird aus /richtexteditor/ geladen. Ein kleines gemeinsames Skript, rte-files.js, verbindet ihn mit dem Server: Es setzt den Upload-Handler des Editors für Drag & Drop, Einfügen und den Dialog „Insert Image“ sowie den, mit dem die Galerie in den geöffneten Ordner hochlädt. Beide senden an /rte/upload und lesen {"url": "..."} aus der Antwort.
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css" />
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<script src="/rte-files.js"></script>var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "/rte/gallery",
maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
});maxUploadFileSize lässt den Editor eine Datei über 4,000,000 Bytes ablehnen, bevor er sie sendet. Der Server prüft die Größe trotzdem selbst.
Die Controller-Action
Im MVC-Beispiel ist die Route eine Action auf einem [ApiController]. [ApiController] bindet einen Parameter vom Typ IFormFile von sich aus aus dem Formular.
// POST /rte/upload file, folder
[HttpPost("upload")]
[RequestSizeLimit(RteFiles.MaxRequestBytes)] // the whole request body
[RequestFormLimits(MultipartBodyLengthLimit = RteFiles.MaxRequestBytes)] // the multipart parser
public async Task<IActionResult> Upload(IFormFile? file, [FromForm] string? folder) =>
Send(await RteFiles.Save(HttpContext, file, folder));Der Parameter ist absichtlich IFormFile?. Bei aktivierten Nullable Reference Types gilt ein nicht nullbarer Parameter als erforderlich, und ein Request ohne Datei würde die ProblemDetails von [ApiController] erhalten statt des {"error": "..."}, das der Editor anzeigt. Die beiden Attribute senken das Request-Limit nur für diese Action. Größenlimits für Uploads in ASP.NET Core behandelt sie.
Dieselbe Route als Minimal API
Das Minimal-API-Beispiel bildet die Route mit MapPost ab. Das Attribut wird zu Endpoint-Metadaten, und WithFormOptions setzt das Multipart-Limit:
rte.MapPost("/upload", async (HttpContext http, IFormFile? file, [FromForm] string? folder) =>
Json(await RteFiles.Save(http, file, folder)))
.DisableAntiforgery()
// Kestrel accepts 30,000,000-byte bodies by default; this route needs ~4 MB.
.WithMetadata(new RequestSizeLimitAttribute(RteFiles.MaxRequestBytes))
.WithFormOptions(multipartBodyLengthLimit: RteFiles.MaxRequestBytes);.DisableAntiforgery() ist in .NET 8 nicht optional. Ein Endpoint, der IFormFile oder [FromForm] bindet, trägt Antiforgery-Metadaten. Ohne app.UseAntiforgery() schlug jeder POST daran mit einem 500 fehl („contains anti-forgery metadata, but a middleware was not found“). Mit der Middleware erhielt ein POST ohne Token ein 400. Die Requests des Editors enthalten kein Token, deshalb schaltet das Beispiel die Prüfung ab und prüft stattdessen den Origin des Requests. Sicherer Bild-Upload in ASP.NET Core erklärt diese Prüfung.
Was mit der Datei passiert
Beide Varianten rufen dieselbe Methode auf:
// POST /rte/upload file=<image>, folder=<existing folder, optional>
public static Task<RteResult> Save(HttpContext http, IFormFile? file, string? folder) => RunAsync(async () =>
{
RequireSameOrigin(http.Request);
var root = RootFor(http);
folder ??= "";
var disk = Resolve(root, folder);
if (file is null || file.Length == 0) throw new RteError(400, "No file was uploaded.");
if (file.Length > MaxFileBytes) throw new RteError(413, "The image is larger than 4 MB.");
var ext = Path.GetExtension(file.FileName).ToLowerInvariant();
if (!Extensions.Contains(ext))
throw new RteError(400, "Only .jpg, .jpeg, .png, .gif and .webp images can be uploaded.");
// file.ContentType is whatever the client claimed. Check the first bytes instead.
var head = new byte[12];
await using (var input = file.OpenReadStream())
await input.ReadAtLeastAsync(head, head.Length, throwOnEndOfStream: false);
if (!SignatureMatches(ext, head)) throw new RteError(400, $"The file is not a valid {ext} image.");
// A new name every time, so an upload never overwrites another file.
var random = Convert.ToHexString(RandomNumberGenerator.GetBytes(4)).ToLowerInvariant();
var stored = $"{SafeBaseName(file.FileName)}-{random}{ext}";
await using (var output = new FileStream(Path.Combine(disk, stored), FileMode.CreateNew))
await file.CopyToAsync(output);
return new { url = UrlFor(root, folder, stored) };
});- Der Ordner ist ein virtueller Pfad wie
blog, der unteruploads/bereits existieren muss. Er wird nie direkt als Pfad auf der Festplatte verwendet. - Die Dateiendung muss
.jpg,.jpeg,.png,.gifoder.webpsein, und die ersten Bytes müssen zu ihr passen.file.ContentTypewird ignoriert: Es ist das, was der Client in den Request geschrieben hat. - Der gespeicherte Name ist jedes Mal neu: Aus
Red Square.pngwirdred-square-1a2b3c4d.png.FileMode.CreateNewbedeutet, dass eine vorhandene Datei nie überschrieben wird. - Die Dateien werden unter
/uploads/...über eine Zuordnung für statische Dateien ausgeliefert, die nur die fünf Bildtypen kennt.
Probleme kommen als JSON mit einem Status zurück, den der Editor anzeigen kann: 400 für eine ungültige Datei oder einen ungültigen Ordner, 403 für einen Cross-Site-Request, 404 für einen fehlenden Ordner, 413 für eine Datei über 4,000,000 Bytes.
Testen
Starten Sie das Beispielprojekt zu dieser Anleitung mit dotnet run (es braucht das .NET 8 SDK) und öffnen Sie http://localhost:5302. Die Upload-Schaltfläche und die Galerie funktionieren beide. Senden Sie in einer Shell ein Foto in den Stammordner und in einen Ordner:
curl -F "file=@photo.jpg" http://localhost:5302/rte/upload
{"url":"/uploads/photo-d78dc40d.jpg"}
curl -F "file=@photo.jpg" -F "folder=blog" http://localhost:5302/rte/upload
{"url":"/uploads/blog/photo-65fc24fe.jpg"}Das Minimal-API-Beispiel antwortet auf Port 5301 genauso. Ein Ordner, der dort noch nicht existiert, erhält {"error":"Folder not found."} mit einem 404, weil Uploads nie Ordner anlegen. Das tut die Schaltfläche „New folder“ der Galerie.
Dieselbe Anleitung für andere Plattformen
JavaScript · Next.js · PHP · Razor Pages · Blazor · MVC 5 · Web Forms