Adicionar uma galeria de imagens a um editor de texto rico no ASP.NET Core

Um botão de upload só adiciona arquivos. A caixa de diálogo Insert Gallery também mostra o que já está no servidor e permite abrir pastas, criar novas e enviar arquivos para a pasta aberta. No ASP.NET Core, a caixa de diálogo precisa de uma rota GET que lista uma pasta como JSON, uma rota POST para novas pastas e a rota de upload. Esta página trata da listagem e de como os arquivos são servidos.
O botão e a opção
A barra de ferramentas full já tem o botão Insert Gallery (insertgallery). Definir galleryEndpoint faz a caixa de diálogo trocar uma lista fixa de imagens predefinidas pelo seu servidor:
var editor = new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "/rte/gallery",
maxUploadFileSize: 4000000 // same limit as the server, checked before the upload starts
});A caixa de diálogo acrescenta ?folder= a essa URL para listar uma pasta e envia action=create-folder por POST para a mesma URL quando o botão New folder é usado. Os uploads passam por rte-files.js até /rte/upload.
A rota
// GET /rte/gallery?folder=products
[HttpGet("gallery")]
public IActionResult Gallery([FromQuery] string? folder) =>
Send(RteFiles.List(HttpContext, folder));No exemplo de Minimal API, a mesma linha é rte.MapGet("/gallery", (HttpContext http, string? folder) => ...). As duas chamam 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 transforma a pasta virtual em um caminho dentro de uploads/ ou responde 400 ou 404. Pastas da galeria no ASP.NET Core explica como.
O JSON que a caixa de diálogo lê
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);
}
parentFolderénullna raiz, o que desativa o botão Up da caixa de diálogo. Dentro de uma pasta, é o caminho virtual da pasta pai,""para uma pasta de primeiro nível.- As propriedades saem em camelCase porque os padrões de JSON do ASP.NET Core são camelCase, tanto para
Results.Jsonquanto para oObjectResultdo MVC. - Só os cinco tipos de imagem são listados. Pastas cujos nomes não passam pelas regras de pasta, como
.git, são ignoradas. - Os nomes são ordenados pela forma em minúsculas com comparação ordinal.
StringComparer.OrdinalIgnoreCasecompara em maiúsculas, então ordenaa_beaabde forma diferente do navegador. - Cada segmento da URL é escapado com
Uri.EscapeDataString, então uma pasta chamadaMy photosainda gera uma URL válida.
Servindo as imagens
As URLs apontam para /uploads/, servido por uma segunda chamada a UseStaticFiles com sua própria lista de content types:
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",
});A lista importa. Com o FileExtensionContentTypeProvider padrão, um arquivo .html ou .svg na pasta seria servido como text/html ou image/svg+xml. Desativar ServeUnknownFileTypes, que já é o padrão, só bloqueia extensões que o provider não conhece. Com a lista do exemplo, um probe.html e um probe.svg que colocamos em uploads/ retornaram 404 nos dois casos.
O que a caixa de diálogo faz hoje
A caixa de diálogo do editor lançado diz "Accepted formats: JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF". Este servidor aceita cinco deles. Um .bmp é enviado, o servidor responde 400 e a caixa de diálogo mostra sua própria mensagem, "Upload failed for pic.bmp.", em vez da mensagem do servidor. Um arquivo acima de maxUploadFileSize é recusado no navegador com "big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB." A caixa de diálogo conta em megabytes binários.
Teste
Execute o projeto de exemplo deste guia com dotnet run, abra http://localhost:5302 e clique em Insert Gallery. Para ver o JSON sem a caixa de diálogo:
curl "http://localhost:5302/rte/gallery?folder=blog%2F2026"
{"currentFolder":"blog/2026","currentFolderDisplay":"/blog/2026","parentFolder":"blog","folders":[],"images":[]}Essa pasta foi criada com o botão New folder. Uma pasta que não existe responde 404 {"error":"Folder not found."}.
O mesmo guia para outras plataformas
JavaScript · Next.js · PHP · Razor Pages · Blazor · MVC 5 · Web Forms