0

I am working on an ASP.NET Core 2.1 API project, which will be consumed by an Angular App, and later on a mobile app. one of the required functionality is to zip and download a collection of files, the result zipped file is going to be large (1GB or more). the code I have now is working as follows:

  1. the Angular app requests the API and expects a blob response.
  2. the API on the server creates a Zip file and reads it using memory stream.
  3. the API returns the memory stream using File response.
  4. the method that subscribes to the download service in Angular saves the file.

what is happening now is when I click in the browser on the download button I have to wait for the download to be finished then the browser shows the default popup that allows the user to save and select where to save.

I was wondering if what I'm doing is correct and won't cause any memory problems in the future?

is there a better methodology where the file could be streamed smoothly, so when the download starts the browser directly shows the save message and shows the default browser progress bar?

angular code:

component click function:

download(event,id){
    event.stopPropagation();
    event.preventDefault();
    
    this.myservice.Downloadservice(avatar_id).subscribe((res: any) => {
    
      saveAs(res.data, res.filename); 
    });
  }

service code:

DownloadAllservice(id): Observable<any> {
    let authToken = localStorage.getItem('auth_token');
    let _options = { headers: new Headers({ 'Authorization': `Bearer ${authToken}`, } ),responseType: ResponseContentType.Blob };
    let formData = new FormData();
    let options ={
      type: "zip",
      id: id
    };
    formData.append('options',JSON.stringify(options));
    
    return this.http.post(this.baseUrl + "/api/Download/", formData, _options)
      .map(response =>{
     
       return {
          'filename': this.getFileNameFromHttpResponse(response),
         'data': response.blob()
      } })
      .catch(this.handleError);
  }

.net core code:

[Authorize(Policy = "Admin")]
        [DisableRequestSizeLimit]
        [HttpPost("Download", Name = "Download")]
        public async Task<IActionResult> Download()
        {
            // get files list then start creating the temp folder and zipped folder



            var archive = Path.Combine(Directory.GetDirectoryRoot("wwwroot"), @"home\" + FilePath + @"\temp\" + "filename");
            var temp = Path.Combine(Directory.GetDirectoryRoot("wwwroot"), @"home\" + FilePath + @"\temp");
            if (!System.IO.Directory.Exists(temp))
            {
                Directory.CreateDirectory(temp);
            }
           
               
                Directory.CreateDirectory(archive);

            try
            {
               
                foreach (var file_id in filelist)
                {
                   
                    var path = file.path;
                    if (!System.IO.File.Exists(Path.Combine(archive, Path.GetFileName(path)))) {
                        System.IO.File.Copy(path, Path.Combine(archive, Path.GetFileName(path)));
                    }
                    

                }
              
                var archivezip = Path.Combine(Directory.GetDirectoryRoot("wwwroot"), @"home\" + FilePath + @"\temp\" + "filename" + ".zip");

                // create a new archive
                ZipFile.CreateFromDirectory(archive, archivezip);
               
                var memory = new MemoryStream();

                using (var stream = new FileStream(archivezip, FileMode.Open))
                {
                    await stream.CopyToAsync(memory);
                }
                memory.Position = 0;
                Directory.EnumerateFiles(archive).ToList().ForEach(f => System.IO.File.Delete(f));
                Directory.EnumerateDirectories(archive).ToList().ForEach(f => System.IO.Directory.Delete(f, true));
                Directory.Delete(archive);
                System.IO.File.Delete(archivezip);
                return File(memory, "application/octet-stream","filename.zip");

            }
            catch (Exception ex)
            {

                return new BadRequestObjectResult(ex.Message);
            }
        }

please note that in the future not only angular app will use the API, but mobile apps will also be added

Moneer Kamal
  • 1,837
  • 16
  • 25
  • 1
    This is *already* causing memory problems - it's eating up 1GB of RAM to hold data that could be read from a disk file. You should return the ZIP file directly and look for ways to delete it once it's returned. This could be as simple as [inheriting from one of the FileResult classes](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.fileresult?view=aspnetcore-5.0) and overriding `Execute` or `ExecuteAsync` to delete the source file once downloading completes – Panagiotis Kanavos Dec 03 '20 at 10:36
  • I can't believe I had already answered such a question and forgot about it – Panagiotis Kanavos Dec 03 '20 at 10:47
  • thanks, @PanagiotisKanavos, yes that is one of my concerns that I'm wasting the RAM – Moneer Kamal Dec 03 '20 at 11:00

1 Answers1

1

I do a similar thing (I guess everyone does because that's apparently how it's done with Angular). I just do a basic unlimited loading spinner because, so far, I haven't needed to worry about tracking the progress.

However, there are various guides to handling this out there for you to follow.

It seems to boil down to changing your request from a simple, standard get/post request to one that listens to the events of the response.

From the linked article:

this.http.get(url, {
  reportProgress: true,
  observe: 'events',
  responseType: 'blob'
})

Important parts there are to ensure it wants progress reports and to observe events. Once the request is observing events, then you need to handle observing them (obviously).

That, however, is a longer more involved part of the article. Go forth and good luck.

Edit: Ah, the issue is actually API side you're worried about. Fair enough, there are a variety of similar questions that might be of use then.

Krenom
  • 1,894
  • 1
  • 13
  • 20