ASP.NET Core でリッチテキストエディターに画像ギャラリーを追加する

「Insert Gallery」ダイアログ。ASP.NET Core のサンプルの uploads フォルダーにある blog、products、team の各フォルダーと 4 枚の写真が表示され、1 枚が選択されている

アップロードボタンでできるのは、ファイルを追加することだけです。「Insert Gallery」ダイアログでは、すでにサーバーにあるものを表示し、フォルダーを開いたり、新しいフォルダーを作成したり、開いているフォルダーにアップロードしたりできます。ASP.NET Core でこのダイアログを使うには、フォルダーの一覧を JSON で返す GET ルート、新しいフォルダー用の POST ルート、そしてアップロード用のルートが必要です。このページでは、一覧の作成とファイルの配信方法を説明します。

ボタンとオプション

full ツールバーには、すでに「Insert Gallery」ボタン(insertgallery)があります。galleryEndpoint を設定すると、ダイアログは固定のプリセット画像の一覧ではなく、サーバーの画像を表示するようになります。

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
});

ダイアログは、フォルダーを一覧表示するときにはその URL に ?folder= を付け、「New folder」ボタンでは同じ URL に action=create-folder を POST します。アップロードは rte-files.js を通じて /rte/upload に送られます。

ルート

C# · Controllers/RteController.cs
// GET /rte/gallery?folder=products
[HttpGet("gallery")]
public IActionResult Gallery([FromQuery] string? folder) =>
    Send(RteFiles.List(HttpContext, folder));

最小 API のサンプルでは、同じ処理は rte.MapGet("/gallery", (HttpContext http, string? folder) => ...) の 1 行です。どちらも 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 は、仮想フォルダーを uploads/ 内のパスに変換するか、400 または 404 を返します。これについては ASP.NET Core のギャラリーフォルダーで説明しています。

ダイアログが読み取る JSON

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);
}
ASP.NET Core のサンプルが GET /rte/gallery?folder=blog に対して返した JSON。currentFolder、currentFolderDisplay、parentFolder、空の folders リスト、url、name、folder、size を持つ画像がある
blog フォルダーに対する実際のレスポンス。画像は 2 つに省略しています。
  • ルートでは parentFolder が null になり、ダイアログの「Up」ボタンが無効になります。フォルダー内では親の仮想パスで、最上位のフォルダーなら "" です。
  • プロパティがキャメルケースで出力されるのは、Results.Json でも MVC の ObjectResult でも、ASP.NET Core の JSON の既定がキャメルケースだからです。
  • 一覧に含めるのは 5 種類の画像だけです。.git のように、名前がフォルダーのルールに合わないフォルダーはスキップします。
  • 名前は小文字にした形を序数比較で並べ替えます。StringComparer.OrdinalIgnoreCase は大文字にして比較するため、a_b と aab の順序がブラウザーと異なってしまいます。
  • URL の各セグメントは Uri.EscapeDataString でエスケープするので、My photos というフォルダーでも正しく機能する URL になります。

画像の配信

URL は /uploads/ を指しており、独自のコンテンツタイプの一覧を持つ 2 つ目の UseStaticFiles の呼び出しで配信されます。

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",
});

この一覧が重要です。既定の FileExtensionContentTypeProvider では、フォルダー内の .html や .svg のファイルが、text/html や image/svg+xml として配信されてしまいます。ServeUnknownFileTypes を無効にしても(これが既定です)、ブロックされるのはプロバイダーが知らない拡張子だけです。サンプルの一覧を使うと、uploads/ に置いた probe.html と probe.svg はどちらも 404 になりました。

現在のダイアログの動作

リリース版のエディターのダイアログには「Accepted formats: JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF」と表示されます。このサーバーが受け付けるのはそのうち 5 つです。.bmp は送信されますが、サーバーは 400 を返し、ダイアログはサーバーのメッセージではなく、独自の「Upload failed for pic.bmp.」を表示します。maxUploadFileSize を超えるファイルは、ブラウザーで「big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB.」として拒否されます。ダイアログは 2 進数のメガバイトで計算します。

テストする

このガイドのサンプルプロジェクトを dotnet run で実行し、http://localhost:5302 を開いて「Insert Gallery」をクリックします。ダイアログを使わずに JSON を確認するには、以下を実行します。

Shell
curl "http://localhost:5302/rte/gallery?folder=blog%2F2026"
{"currentFolder":"blog/2026","currentFolderDisplay":"/blog/2026","parentFolder":"blog","folders":[],"images":[]}

このフォルダーは「New folder」ボタンで作成したものです。存在しないフォルダーを指定すると、404 {"error":"Folder not found."} が返ります。

ほかのプラットフォーム向けの同じガイド

JavaScript · Next.js · PHP · Razor Pages · Blazor · MVC 5 · Web Forms

次に読む