Question: How to download a ZipFile from a dotnet core webapi?

Question

How to download a ZipFile from a dotnet core webapi?

Answers 1
Added at 2016-12-29 17:12
Tags
Question

I am trying to download a zip file from a dotnet core web api action, but I can't make it work. I tried calling the action via POSTMAN and my Aurelia Http Fetch Client.

I am able to create the ZipFile like I want it and store it on the system, but can't fix it so it returns the zipfile via the api.

Use-case: User selects a couple of picture collections and clicks the download button. The ids of the picture collections gets send to the api and a zipfile is created which contains a directory for every picture collection which holds the pictures. That zipfile is returned to the user so he/she can store it on their system.

Any help would be appreciated.

My controller action

      /// <summary>
      /// Downloads a collection of picture collections and their pictures
      /// </summary>
      /// <param name="ids">The ids of the collections to download</param>
      /// <returns></returns>
      [HttpPost("download")]
      [ProducesResponseType(typeof(void), (int) HttpStatusCode.OK)]
      public async Task<IActionResult> Download([FromBody] IEnumerable<int> ids)
      {
           // Create new zipfile
           var zipFile = $"{_ApiSettings.Pictures.AbsolutePath}/collections_download_{Guid.NewGuid().ToString("N").Substring(0,5)}.zip";

           using (var repo = new PictureCollectionsRepository())
           using (var picturesRepo = new PicturesRepository())
           using (var archive = ZipFile.Open(zipFile, ZipArchiveMode.Create))
           {
                foreach (var id in ids)
                {
                     // Fetch collection and pictures
                     var collection = await repo.Get(id);
                     var pictures = await picturesRepo
                          .GetAll()
                          .Where(x => x.CollectionId == collection.Id)
                          .ToListAsync();

                     // Create collection directory IMPORTANT: the trailing slash
                     var directory = $"{collection.Number}_{collection.Name}_{collection.Date:yyyy-MM-dd}/";
                     archive.CreateEntry(directory);

                     // Add the pictures to the current collection directory
                     pictures.ForEach(x => archive.CreateEntryFromFile(x.FilePath, $"{directory}/{x.FileName}"));
                }
           }

           // What to do here so it returns the just created zip file?
      }
 }

My aurelia fetch client function:

/**
 * Downloads all pictures from the picture collections in the ids array
 * @params ids The ids of the picture collections to download
 */
download(ids: Array<number>): Promise<any> {
    return this.http.fetch(AppConfiguration.baseUrl + this.controller + 'download', {
        method: 'POST',
        body: json(ids)
    })
}

What I've tried

Note that what I've tried does not generate errors, it just doesn't seems to do anything.

1) Creating my own FileResult (like I used to do with older ASP.NET). Can't see the headers being used at all when I call it via postman or the application.

return new FileResult(zipFile, Path.GetFileName(zipFile), "application/zip");

 public class FileResult : IActionResult
 {
      private readonly string _filePath;
      private readonly string _contentType;
      private readonly string _fileName;

      public FileResult(string filePath, string fileName = "", string contentType = null)
      {
           if (filePath == null) throw new ArgumentNullException(nameof(filePath));

           _filePath = filePath;
           _contentType = contentType;
           _fileName = fileName;
      }

      public Task ExecuteResultAsync(ActionContext context)
      {
           var response = new HttpResponseMessage(HttpStatusCode.OK)
           {
                Content = new ByteArrayContent(System.IO.File.ReadAllBytes(_filePath))
           };

           if (!string.IsNullOrEmpty(_fileName))
                response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
                {
                     FileName = _fileName
                };

           response.Content.Headers.ContentType = new MediaTypeHeaderValue(_contentType);

           return Task.FromResult(response);
      }
 }

}

2) http://stackoverflow.com/a/34857134/2477872

Does nothing.

      HttpContext.Response.ContentType = "application/zip";
           var result = new FileContentResult(System.IO.File.ReadAllBytes(zipFile), "application/zip")
           {
                FileDownloadName = Path.GetFileName(zipFile)
           };
           return result;

I've tried it with a test dummy PDF file and that seemed to work with POSTMAN. But when I try to change it to the zipfile (see above) it does nothing.

  HttpContext.Response.ContentType = "application/pdf";
           var result = new FileContentResult(System.IO.File.ReadAllBytes("THE PATH/test.pdf"), "application/pdf")
           {
                FileDownloadName = "test.pdf"
           };

           return result;
Answers
nr: #1 dodano: 2016-12-30 12:12

To put a long story short, the example below illustrates how to easily serve both a PDF as well as a ZIP through a dotnet-core api:

/// <summary>
/// Serves a file as PDF.
/// </summary>
[HttpGet, Route("{filename}/pdf", Name = "GetPdfFile")]
public IActionResult GetPdfFile(string filename)
{
    const string contentType = "application/pdf";
    HttpContext.Response.ContentType = contentType;
    var result = new FileContentResult(System.IO.File.ReadAllBytes(@"{path_to_files}\file.pdf"), contentType)
    {
        FileDownloadName = $"{filename}.pdf"
    };

    return result;
}

/// <summary>
/// Serves a file as ZIP.
/// </summary>
[HttpGet, Route("{filename}/zip", Name = "GetZipFile")]
public IActionResult GetZipFile(string filename)
{
    const string contentType ="application/zip";
    HttpContext.Response.ContentType = contentType;
    var result = new FileContentResult(System.IO.File.ReadAllBytes(@"{path_to_files}\file.zip"), contentType)
    {
        FileDownloadName = $"{filename}.zip"
    };

    return result;
}

This sample just works™

Notice in this case there is only one main difference between the two actions (besied the source file name, of course): the contentType that is returned.

The example above uses 'application/zip', as you've mentioned yourself, but it might just be required to serve a different mimetype (like 'application/octet*').

This leads to the speculation that either the zipfile cannot be read properly or that your webserver configuration might not be configured properly for serving .zip files.

The latter may differ based on whether you're running IIS Express, IIS, kestrel etc. But to put this to the test, you could try adding a zipfile to your ~/wwwroot folder, making sure you have enabled serving static files in your Status.cs, to see if you can download the file directly.

Source Show
◀ Wstecz