How to Create a Multipart HTTP Response With ASP.NET Core

后端 未结 2 1687
遥遥无期
遥遥无期 2021-02-07 09:44

I would like to create an action method in my ASP.NET Core controller which returns a Multipart HTTP Response containing several files. I know that using a .zip file is the reco

相关标签:
2条回答
  • 2021-02-07 10:21

    From MSDN

    MSDN has a document that lists a lot of the multipart subtypes. The multipart/byteranges seems most appropriate for sending multiple files in an HTTP Response for download by the client application. The bold part is particularly relevant.

    The multipart/byteranges content type is defined as a part of the HTTP message protocol. It includes two or more parts, each with its own Content-Type and Content-Range fields. The parts are separated using a MIME boundary parameter. It allows for binary as well as 7-bit and 8-bit files to be sent as multiple parts with the lengths of the parts being specified in the header of each part. Note that while HTTP makes provisions for using MIME for HTTP documents, HTTP is not strictly MIME-compliant. (Emphasis added.)

    From RFC2068

    RFC2068, section 19.2 provides a description of multipart/byteranges. Again, the bold part is relevant. Each byterange can have its own Content-type and it turns out can also have its own Content-disposition.

    The multipart/byteranges media type includes two or more parts, each with its own Content-Type and Content-Range fields. The parts are separated using a MIME boundary parameter. (Emphasis added.)

    The RFC also provides this technical definition:

    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
    

    The best part of the RFC is its example, which the ASP.NET Core sample below illustrates.

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

    Note that they are sending two PDFs! That is just what you're needing.

    One ASP.NET Core Approach

    Here is a code sample that works on Firefox. That is, Firefox downloads three image files, which we can open with Paint. The source is on GitHub.

    The sample uses app.Run(). To adapt the sample to a controller action, inject IHttpContextAccessor into your controller and write to _httpContextAccessor.HttpContext.Response in your action method.

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

    Note: this sample code requires refactoring before reaching production.

    0 讨论(0)
  • 2021-02-07 10:23

    I've written a more generic MultipartResult class which just inherits from ActionResult:

    Usage Example

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

    Implementation

    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 讨论(0)
提交回复
热议问题