10

我想在我的ASP.NET Core控制器中创建一个操作方法,该方法返回包含多个文件的Multipart HTTP Response。我知道使用.zip文件是网站推荐的方法,但我正在考虑使用这样的API请求。如何使用ASP.NET Core创建多部分HTTP响应

ASP.NET Core示例中能够使用find的示例是在上载文件时处理多部分HTTP请求。在我的情况下,我想下载文件。

UPDATE

我提出了以下问题GitHub的:#4933

回答

6

我写了一个更通用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); 
    } 
} 
+0

在你的问题中,你提到这个“不是从浏览器下载文件的好方法,因为缺乏支持,但我正在编写一个API并拥有一个我也控制的客户端。”在Shaun Luttin的回答中,他特别提到了Firefox。为了澄清,您的答案适用于哪些场景? – chris

+0

这将适用于您想要返回多个文件的API。就我而言,我需要减少由于高延迟造成的HTTP请求数量。由于Firefox是唯一支持的浏览器(我尝试过),因此这对于要下载多个文件的网站无效。我希望有所帮助。 –

+0

我该如何做到相反:我有一个控制器在其请求中有一个'multipart/mixed'。我如何阅读各个部分/流? – Shimmy

6

从MSDN

MSDN has a document that lists a lot of the multipart subtypes.multipart/byteranges似乎最适合由客户端的HTTP响应发送多个文件下载应用。大胆的部分是特别相关的。

多部分/字节范围内容类型被定义为HTTP消息协议的一部分。它包含两个或多个部分,每个部分都有自己的Content-Type和Content-Range字段。部件使用MIME边界参数分隔。 它允许二进制以及7位和8位文件作为多个部分发送,其中每个部分的标题中指定部分的长度。请注意,尽管HTTP为HTTP文档使用MIME做了规定,但HTTP并不严格遵从MIME。 (着重。)

从RFC2068

RFC2068,部分19.2提供的multipart/byteranges的描述。再次,大胆的部分是相关的。每个字节范围可以有它自己的Content-type,事实证明它也可以有它自己的Content-disposition

多部分/字节范围媒体类型包括两个或更多部分,每与它自己的内容类型和内容的范围字段。部件使用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.NET核心示例说明。

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-- 

请注意,他们发送两个PDF!这正是你需要的。

一个ASP.NET核心方法

这是一个在Firefox上工作的代码示例。也就是说,Firefox下载三个图像文件,我们可以用Paint打开。 The source is on 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); 
    } 
} 

注意:此示例代码在达到生产前需要重构。

+0

谢谢你的好先生! –

+2

事实证明,我并不需要Conte-range HTTP头,如果您正在检索文件的特定部分而不是整个文件,则会使用它。 –

相关问题