Añadir una galería de imágenes a un editor de texto enriquecido en ASP.NET Core

Un botón de carga solo añade archivos. El cuadro de diálogo Insert Gallery también muestra lo que ya está en el servidor y permite abrir carpetas, crear otras nuevas y subir archivos a la carpeta abierta. En ASP.NET Core, el cuadro de diálogo necesita una ruta GET que enumere una carpeta como JSON, una ruta POST para las carpetas nuevas y la ruta de carga. Esta página trata el listado y cómo se sirven los archivos.
El botón y la opción
La barra de herramientas full ya incluye el botón Insert Gallery (insertgallery). Al definir galleryEndpoint, el cuadro de diálogo pasa de una lista fija de imágenes predefinidas a tu servidor:
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "/rte/gallery",
maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
});El cuadro de diálogo añade ?folder= a esa URL para enumerar una carpeta, y envía action=create-folder a la misma URL para el botón New folder. Las cargas van a /rte/upload a través de rte-files.js.
La ruta
// GET /rte/gallery?folder=products
[HttpGet("gallery")]
public IActionResult Gallery([FromQuery] string? folder) =>
Send(RteFiles.List(HttpContext, folder));En el ejemplo de Minimal API, la misma línea es rte.MapGet("/gallery", (HttpContext http, string? folder) => ...). Ambos llaman a RteFiles.List:
// GET /rte/gallery?folder=products
public static RteResult List(HttpContext http, string? folder) => Run(() =>
{
var root = RootFor(http);
folder ??= "";
return Listing(root, folder, Resolve(root, folder));
});Resolve convierte la carpeta virtual en una ruta dentro de uploads/ o responde 400 o 404. Carpetas de la galería en ASP.NET Core lo explica.
El JSON que lee el cuadro de diálogo
static RteListing Listing(RteRoot root, string folder, string disk)
{
var dir = new DirectoryInfo(disk);
var folders = dir.EnumerateDirectories()
.Where(d => IsValidName(d.Name))
.OrderBy(d => d.Name.ToLowerInvariant(), StringComparer.Ordinal)
.Select(d => new RteFolder(folder.Length == 0 ? d.Name : folder + "/" + d.Name, d.Name))
.ToList();
var images = dir.EnumerateFiles()
.Where(f => !f.Name.StartsWith('.') && Extensions.Contains(f.Extension.ToLowerInvariant()))
.OrderBy(f => f.Name.ToLowerInvariant(), StringComparer.Ordinal)
.Select(f => new RteImage(UrlFor(root, folder, f.Name), f.Name, folder, f.Length))
.ToList();
string? parent = folder.Length == 0 ? null : folder.Contains('/') ? folder[..folder.LastIndexOf('/')] : "";
return new RteListing(folder, "/" + folder, parent, folders, images);
}
parentFolderesnullen la raíz, lo que desactiva el botón Up del cuadro de diálogo. En una carpeta es la ruta virtual de la carpeta superior,""para una carpeta de primer nivel.- Las propiedades salen en camelCase porque la configuración JSON predeterminada de ASP.NET Core usa camelCase, tanto para
Results.Jsoncomo para elObjectResultde MVC. - Solo se enumeran los cinco tipos de imagen. Las carpetas cuyos nombres no cumplen las reglas de carpetas, como
.git, se omiten. - Los nombres se ordenan por su forma en minúsculas con una comparación ordinal.
StringComparer.OrdinalIgnoreCasecompara en mayúsculas, así que ordenaa_byaabde forma distinta al navegador. - Cada segmento de la URL se escapa con
Uri.EscapeDataString, así que una carpeta llamadaMy photossigue dando una URL que funciona.
Servir las imágenes
Las URL apuntan a /uploads/, que sirve una segunda llamada a UseStaticFiles con su propia lista de tipos de contenido:
var uploads = RteFiles.UploadsFolder(app.Environment);
Directory.CreateDirectory(uploads);
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(uploads),
RequestPath = "/uploads",
ContentTypeProvider = new FileExtensionContentTypeProvider(new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
[".jpg"] = "image/jpeg", [".jpeg"] = "image/jpeg", [".png"] = "image/png",
[".gif"] = "image/gif", [".webp"] = "image/webp",
}),
OnPrepareResponse = ctx => ctx.Context.Response.Headers.XContentTypeOptions = "nosniff",
});La lista importa. Con el FileExtensionContentTypeProvider predeterminado, un archivo .html o .svg de la carpeta se serviría como text/html o image/svg+xml. Desactivar ServeUnknownFileTypes, que es el valor predeterminado, solo bloquea las extensiones que el proveedor no conoce. Con la lista del ejemplo, un probe.html y un probe.svg que pusimos en uploads/ devolvieron ambos 404.
Qué hace hoy el cuadro de diálogo
El cuadro de diálogo del editor publicado dice "Accepted formats: JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF". Este servidor acepta cinco de ellos. Un .bmp se envía, el servidor responde 400 y el cuadro de diálogo muestra su propio "Upload failed for pic.bmp." en lugar del mensaje del servidor. Un archivo que supera maxUploadFileSize se rechaza en el navegador con "big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB." El cuadro de diálogo cuenta en megabytes binarios.
Pruébalo
Ejecuta el proyecto de ejemplo de esta guía con dotnet run, abre http://localhost:5302 y haz clic en Insert Gallery. Para ver el JSON sin el cuadro de diálogo:
curl "http://localhost:5302/rte/gallery?folder=blog%2F2026"
{"currentFolder":"blog/2026","currentFolderDisplay":"/blog/2026","parentFolder":"blog","folders":[],"images":[]}Esa carpeta se creó con el botón New folder. Una carpeta que no existe responde 404 {"error":"Folder not found."}.
La misma guía para otras plataformas
JavaScript · Next.js · PHP · Razor Pages · Blazor · MVC 5 · Web Forms