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

A caixa de diálogo Insert Gallery mostrando as pastas blog, products e team e quatro fotos da pasta uploads do exemplo em ASP.NET Core, uma delas selecionada

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:

Razor · Views/Home/Index.cshtml
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

C# · Controllers/RteController.cs
// 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:

C# · RteFiles.cs
// 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ê

C# · RteFiles.cs
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);
}
O JSON que o exemplo ASP.NET Core retorna para GET /rte/gallery?folder=blog: currentFolder, currentFolderDisplay, parentFolder, uma lista folders vazia e images com url, name, folder e size
A resposta real para a pasta blog, reduzida a duas imagens.
  • parentFolder é null na 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.Json quanto para o ObjectResult do 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.OrdinalIgnoreCase compara em maiúsculas, então ordena a_b e aab de forma diferente do navegador.
  • Cada segmento da URL é escapado com Uri.EscapeDataString, então uma pasta chamada My photos ainda 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:

C# · RteEndpoints.cs
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:

Shell
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

Leia a seguir