web-dev-qa-db-ja.com

ASP.NETCoreを使用してマルチパートHTTP応答を作成する方法

ASP.NET Coreコントローラーで、複数のファイルを含むマルチパートHTTP応答を返すアクションメソッドを作成したいと思います。 .Zipファイルを使用することがWebサイトに推奨されるアプローチであることは知っていますが、APIに対してそのような要求を使用することを検討しています。

ASP.NET Coreサンプルで私ができた例 find は、ファイルをアップロードするときのマルチパートHTTPリクエストに関するものです。私の場合、ファイルをダウンロードしたいと思います。

[〜#〜]更新[〜#〜]

私は次のGitHubの問題を提起しました: #49

14

MultipartResultから継承するより一般的なActionResultクラスを作成しました。

使用例

[Route("[controller]")]
public class MultipartController : Controller
{
    private readonly IHostingEnvironment hostingEnvironment;

    public MultipartController(IHostingEnvironment hostingEnvironment)
    {
        this.hostingEnvironment = hostingEnvironment;
    }

    [HttpGet("")]
    public IActionResult Get()
    {
        return new MultipartResult()
        {
            new MultipartContent()
            {
                ContentType = "text/plain",
                FileName = "File.txt",
                Stream = this.OpenFile("File.txt")
            },
            new MultipartContent()
            {
                ContentType = "application/json",
                FileName = "File.json",
                Stream = this.OpenFile("File.json")
            }
        };
    }

    private Stream OpenFile(string relativePath)
    {
        return System.IO.File.Open(
            Path.Combine(this.hostingEnvironment.WebRootPath, relativePath),
            FileMode.Open,
            FileAccess.Read);
    }
}

実装

public class MultipartContent
{
    public string ContentType { get; set; }

    public string FileName { get; set; }

    public Stream Stream { get; set; }
}

public class MultipartResult : Collection<MultipartContent>, IActionResult
{
    private readonly System.Net.Http.MultipartContent content;

    public MultipartResult(string subtype = "byteranges", string boundary = null)
    {
        if (boundary == null)
        {
            this.content = new System.Net.Http.MultipartContent(subtype);
        }
        else
        {
            this.content = new System.Net.Http.MultipartContent(subtype, boundary);
        }
    }

    public async Task ExecuteResultAsync(ActionContext context)
    {
        foreach (var item in this)
        {
            if (item.Stream != null)
            {
                var content = new StreamContent(item.Stream);

                if (item.ContentType != null)
                {
                    content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(item.ContentType);
                }

                if (item.FileName != null)
                {
                    var contentDisposition = new ContentDispositionHeaderValue("attachment");
                    contentDisposition.SetHttpFileName(item.FileName);
                    content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
                    content.Headers.ContentDisposition.FileName = contentDisposition.FileName;
                    content.Headers.ContentDisposition.FileNameStar = contentDisposition.FileNameStar;
                }

                this.content.Add(content);
            }
        }

        context.HttpContext.Response.ContentLength = content.Headers.ContentLength;
        context.HttpContext.Response.ContentType = content.Headers.ContentType.ToString();

        await content.CopyToAsync(context.HttpContext.Response.Body);
    }
}
13

MSDNから

MSDNには、多くのマルチパートサブタイプをリストしたドキュメントがあります。 _multipart/byteranges_は、クライアントアプリケーションによるダウンロードのためにHTTP応答で複数のファイルを送信するのに最も適しているようです。太字の部分は特に関連性があります。

Multipart/byterangesコンテンツタイプは、HTTPメッセージプロトコルの一部として定義されています。 2つ以上の部分が含まれ、それぞれに独自のContent-TypeフィールドとContent-Rangeフィールドがあります。部分は、MIME境界パラメーターを使用して分離されます。 バイナリだけでなく、7ビットおよび8ビットファイルを複数の部分として送信することもできます各パーツのヘッダーで指定されているパーツの長さ。 HTTPはHTTPドキュメントにMIMEを使用するためのプロビジョニングを行いますが、HTTPは厳密にはMIMEに準拠していないことに注意してください。 (強調が追加されました。)

RFC2068から

RFC2068 、セクション19.2は_multipart/byteranges_の説明を提供します。繰り返しますが、太字の部分が関連しています。各バイト範囲は独自の_Content-type_を持つことができ、独自の_Content-disposition_を持つこともできます。

Multipart/byterangesメディアタイプには2つ以上のパートが含まれ、それぞれに独自のContent-TypeフィールドとContent-Rangeフィールドがあります。部分は、MIME境界パラメーターを使用して分離されます。 (強調が追加されました。)

RFCは、この技術的な定義も提供します。

_Media Type name:           multipart
Media subtype name:        byteranges
Required parameters:       boundary
Optional parameters:       none
Encoding considerations:   only "7bit", "8bit", or "binary" are permitted
Security considerations:   none
_

RFCの最良の部分はその例であり、以下のASP.NETCoreサンプルが示しています。

_HTTP/1.1 206 Partial content
Date: Wed, 15 Nov 1995 06:25:24 GMT
Last-modified: Wed, 15 Nov 1995 04:58:08 GMT
Content-type: multipart/byteranges; boundary=THIS_STRING_SEPARATES

--THIS_STRING_SEPARATES
Content-type: application/pdf
Content-range: bytes 500-999/8000

...the first range...
--THIS_STRING_SEPARATES
Content-type: application/pdf
Content-range: bytes 7000-7999/8000

...the second range
--THIS_STRING_SEPARATES--
_

彼らは2つのPDFを送信していることに注意してください!それはあなたが必要としているものです。

1つのASP.NETコアアプローチ

これはFirefoxで動作するコードサンプルです。つまり、FirefoxはPaintで開くことができる3つの画像ファイルをダウンロードします。 ソースはGitHubにあります

Firefox downloads the byte ranges.

サンプルはapp.Run()を使用しています。サンプルをコントローラーアクションに適合させるには、コントローラーにIHttpContextAccessorを挿入し、アクションメソッドで__httpContextAccessor.HttpContext.Response_に書き込みます。

_using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

public class Startup
{
    private const string CrLf = "\r\n";
    private const string Boundary = "--THIS_STRING_SEPARATES";
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
    }

    public void Configure(IApplicationBuilder app)
    {
        app.Run(async context =>
        {
            var response = context.Response;
            response.ContentType = $"multipart/byteranges; boundary={Boundary}";

            // TODO Softcode the 'Content-length' header.            
            response.ContentLength = 13646;
            var contentLength = response.ContentLength.Value;

            await response.WriteAsync(Boundary + CrLf);

            var blue = new FileInfo("./blue.jpg");
            var red = new FileInfo("./red.jpg");
            var green = new FileInfo("./green.jpg");

            long start = 0;
            long end = blue.Length;
            await AddImage(response, blue, start, end, contentLength);

            start = end + 1;
            end = start + red.Length;
            await AddImage(response, red, start, end, contentLength);

            start = end + 1;
            end = start + green.Length;
            await AddImage(response, green, start, end, contentLength);

            response.Body.Flush();
        });
    }

    private async Task AddImage(HttpResponse response, FileInfo fileInfo,
        long start, long end, long total)
    {
        var bytes = File.ReadAllBytes(fileInfo.FullName);
        var file = new FileContentResult(bytes, "image/jpg");

        await response
            .WriteAsync($"Content-type: {file.ContentType.ToString()}" + CrLf);

        await response
            .WriteAsync($"Content-disposition: attachment; filename={fileInfo.Name}" + CrLf);

        await response
            .WriteAsync($"Content-range: bytes {start}-{end}/{total}" + CrLf);

        await response.WriteAsync(CrLf);
        await response.Body.WriteAsync(
            file.FileContents,
            offset: 0,
            count: file.FileContents.Length);
        await response.WriteAsync(CrLf);

        await response.WriteAsync(Boundary + CrLf);
    }
}
_

注:このサンプルコードでは、本番環境に到達する前にリファクタリングが必要です。

8
Shaun Luttin