How to return a zip file via AJAX that requests downloading?

I have an ASP.NET Core Action method that I want to return a zip file for download to the client, and it looks like this:

[HttpPost, ValidateModel] public async Task<IActionResult> DownloadCarousel([FromBody]CarouselViewModel model) { File zip = await MediaService.DownloadAndZipCarousel(model.Images, BaseServerPath); Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{zip.Name}.zip\""); return File(zip.Content, "application/zip", zip.Name); } 

zip.Content contains an array of bytes, which I want to return as zip so that the client can load it. However, instead of receiving the zip file, the answer is as follows:

enter image description here

This is my job doing the work. If it matters:

 public async Task<Models.Models.File> DownloadAndZipCarousel(IEnumerable<Image> images, string baseServerPath) { HttpClient client = CreateClient(); var file = new Models.Models.File(); var random = new Random(); using (var archiveStream = new MemoryStream()) using (var archive = new ZipArchive(archiveStream, ZipArchiveMode.Create, true)) { foreach (Image image in images) { ZipArchiveEntry archiveEntry = archive.CreateEntry($"Instagram{random.Next()}.jpg", CompressionLevel.Optimal); using (Stream entryStream = archiveEntry.Open()) using (Stream contentStream = await client.GetStreamAsync(image.StandartResolutionURL)) { await contentStream.CopyToAsync(entryStream); } } file.Name = "RandomZip"; file.Content = archiveStream.ToArray(); } return file; } 

Instead of using MemoryStream I tried using FileStream to check if my code was working and everything was working as expected, so I think that something is wrong with my actions and not with my service.

Can someone please tell me what I am doing wrong here and what I need to do to return the zip code?

+5
source share
5 answers

Having learned that I cannot offer the user to upload a file with a POST request, I applied a different level approach for this problem, which worked fine for me, and I want to share how I solved my problem.

Now I have 2 actions. My first action creates a zip in memory and returns an array of bytes, after which I save the file in cache, so I do not need to save it on the server and retrieve it later, the actions look like this:

 [HttpPost, ValidateModel] public async Task<IActionResult> SaveCarousel([FromBody]CarouselViewModel model) { File zip = await MediaService.ZipCarouselAsync(model.Images, ZipFolder); if(zip == null) { return BadRequest(); } CachingService.Set<File>(zip.Name, zip); return Ok(zip); } 

The second action answers the GET call and gets the file from the cache and returns it for download.

 [HttpGet] public IActionResult DownloadCarousel(string name) { File zip = CachingService.Get<File>(name); if(zip == null) { return BadRequest(); } Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{zip.Name}.zip\""); return File(zip.Content, "application/zip"); } 

Here's what my AJAX call that makes a POST request looks like:

 $.ajax({ method: "POST", url: "/Media/SaveCarousel", data: requestData, contentType: "application/json; charset=utf-8", success: function (data) { window.location = "/Media/DownloadCarousel?name=" + data.name; }, error: function (error) { console.log(error); } }); 

So basically my idea is that when I send a POST I send the data that I need to send and save the zip file in the cache and return the file name. After the file name, I make window.location with the file name in the AJAX success method for an action that receives the file from the cache and returns it to the user for download.

Thus, I do not need to save any file on the server, which means that I also do not need to support the files myself, which I tried to achieve from the very beginning.

0
source

The OP did not know that the call was through AJAX.

Based on OP's own answer. In order not to save the archive to disk, you can do the following.

Instead of downloading and saving the file to POST. Save the necessary URLs for later use when creating the archive. A memory cache is an option.

Storage Abstraction Example

 public interface IStorage { Task<T> GetAsync<T>(string key); Task SetAsync<T>(string key, T item); } 

Any repository implementation should work after you can continue and then access the URLs after.

The implementation of the service may simply contain the necessary information.

 private readonly IStorage cache; public async Task<string> CacheCarouselAsync(IEnumerable<Image> images) { var token = Guid.NewGuid().ToString(); await cache.SetAsync(token, images.Select(image => image.StandartResolutionURL).ToList()); return token; } 

Upon publication, the media service will trade data for the token

 [HttpPost, ValidateModel] public async Task<IActionResult> SaveCarousel([FromBody]CarouselViewModel model) { var token = await MediaService.CacheCarouselAsync(model.Images); // Generates /Media/DownloadCarousel?name={token} var url = Url.Action("DownloadCarousel","Media", new { name = token }); var content = new { location = url }; return Created(url, content); } 

The URL to receive the archive will also be returned to call the client side.

 $.ajax({ method: "POST", url: "/Media/SaveCarousel", data: requestData, contentType: "application/json; charset=utf-8", success: function (data) { //data.location should hold the download path //setting window.location should now allow save prompt on GET window.location = data.location; }, error: function (error) { console.log(error); } }); 

The GET action is only necessary to exchange the token for the generated zip file.

 [HttpGet] public async Task<IActionResult> DownloadCarousel(string name) { var zip = await MediaService.DownloadAndZipCarousel(name); var filename = $"{zip.Name}.zip"; Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{filename}\""); return File(zip.Content, "application/zip"); } 

The token will be used by the service to retrieve URLs from the repository, then download the carousel and create an archive.

 public async Task<File> DownloadAndZipCarousel(string token) { //Get the model data from storage var images = await cache.GetAsync<IEnumerable<string>>(token); var client = CreateClient(); var file = new File(); var random = new Random(); using (var archiveStream = new MemoryStream()) { using (var archive = new ZipArchive(archiveStream, ZipArchiveMode.Create, false)) { foreach (var uri in images) { var imagename = $"Instagram{random.Next()}.jpg"; var archiveEntry = archive.CreateEntry(imagename, CompressionLevel.Optimal); using (var entryStream = archiveEntry.Open()) { using (var contentStream = await client.GetStreamAsync(uri)) { await contentStream.CopyToAsync(entryStream); } } } } file.Name = "RandomZip"; //TODO: derive a better naming system file.Content = archiveStream.ToArray(); } return file; } 
+2
source

Move your lines file.Name and file.Content outside the using block for ZipArchive :

 public async Task<Models.Models.File> DownloadAndZipCarousel(IEnumerable<Image> images, string baseServerPath) { ... using (var archiveStream = new MemoryStream()) { using (var archive = new ZipArchive(archiveStream, ZipArchiveMode.Create, false)) { ... } file.Name = "RandomZip"; file.Content = archiveStream.ToArray(); } return file; } 

ZipArchive , in particular, needs to write zip records during its deletion before capturing an array of content.

+2
source
 file.Content = archiveStream.ToArray(); 

Must be:

 archive.Dispose(); file.Content = archiveStream.ToArray(); 

Or modify your using statements so that archiveStream.ToArray() is called after archive , but before archiveStream too.

ZipArchive.Dispose

Notes

This method completes the archive recording ...

0
source

From the Mike Brind blog: link

you can simplify the code as shown below:

  [HttpPost]
 public FileResult Download (List files)
 {
     var archive = Server.MapPath ("~ / archive.zip");
     var temp = Server.MapPath ("~ / temp");

// clear any existing archive if (System.IO.File.Exists(archive)) { System.IO.File.Delete(archive); } // empty the temp folder Directory.EnumerateFiles(temp).ToList().ForEach(f => System.IO.File.Delete(f)); // copy the selected files to the temp folder files.ForEach(f => System.IO.File.Copy(f, Path.Combine(temp, Path.GetFileName(f)))); // create a new archive ZipFile.CreateFromDirectory(temp, archive); return File(archive, "application/zip", "archive.zip");} 
-1
source

Source: https://habr.com/ru/post/1268807/


All Articles