使用 Response.Complete 事件主体上的响应流从 POST ASP.NET Core Web API 端点下载 Angular 大文件为 null

问题描述 投票:0回答:1

我们有一个 Angular 应用程序,允许用户通过数据库中的 ID 下载图像列表。 ASP.NET Core Web API 使用响应流创建图像 zip 文件并传输它,同时将图像附加到存档。

问题是,对于大型存档文件(我的测试是 2.5 GB,包含大约 2500 个图像),响应正文(客户端)始终为空。对于较小的存档文件没有问题。

一些相关代码:

首先,在客户端,这是组件中的触发器:

modalInstance.componentInstance.onConfirmEvent.subscribe(res => {
      
        this.downloadInProgress = true;
        // workingList contains a list of image ids
        this.prepareDownloadByObject(workingList, res.createFileTree, res.createMetadata)
    })

在下载服务中,我们使用

HttpClient
发布一个包含图像ID列表的对象,然后使用
handleDownloadProgress
检查下载进度。一旦事件类型为
Response
,我们使用主体创建一个对象url并下载文件(这个方法在SO中随处可见):

prepareDownloadByObject(workingList, createFileTree, createMetadata) {
    
   this.downloadByWorkingList(workingList, createFileTree, createMetadata).subscribe(
      res => this.handleDownloadProgress(res, workingList),
      async err => this.handleDownloadError(err)
    )
  }

downloadByWorkingList(workingListDto: IWorkingList,createTreeDirectory:boolean, createMetadata: boolean): Observable<any> {
    
   return this.http.post(`${this.baseUrl}WorkingList/DownloadWorkingList/${createTreeDirectory}/${createMetadata}`,workingListDto,
    {
      observe: 'events',
      responseType: "arraybuffer",
      reportProgress: true
    });
  }

handleDownloadProgress(event, workingList?: IWorkingList) {
   
   if (event.type === HttpEventType.DownloadProgress) {
      //[... UI update ...]
   } else if (event.type === HttpEventType.Response) {
      this.downloadFile(event, workingList);
      //[... UI update ...]
   } else {
      console.log("Unknown event type", event);
   }

downloadFile(data: any, workingList?: IWorkingList) {
    // true
    if (data.body == null) {
      return;
    }

    const downloadedFile = new Blob([data.body], { type: data.body.type });
    const a = document.createElement("a");
    a.setAttribute("style", "display:none;");
    document.body.appendChild(a);    
    a.download = "workingList" +"_"+ this.getFileDateFormat() + ".zip";
    a.href = URL.createObjectURL(downloadedFile);
    a.target = "_blank";
    a.click();
    document.body.removeChild(a);
}

API端-控制器:

[HttpPost("{createTreeDirectory}/{createMetadata}")]
public async Task<IActionResult> DownloadWorkingList(WorkingListDto workingListDto, bool createTreeDirectory, bool createMetadata, CancellationToken token)
{
    try
    {
        if (workingListDto.ImageIds == null || workingListDto.ImageIds.Count == 0)
        {
            return BadRequest("You have to select images to download");
        }

        Response.ContentType = "application/octet-stream";
        Response.Headers.Append("Content-Disposition", "attachment; filename=workingList.zip");
        await Response.StartAsync(token);
                
        await this._workingListAppService.DownloadWorkingList(workingListDto, createTreeDirectory, createMetadata, token, Response.BodyWriter.AsStream()).ConfigureAwait(false);
               
        await Response.CompleteAsync();

        return new EmptyResult();
    }
    catch (ArgumentException ex)
    {
        return this.BadRequest(new { ex.Message, ex.StackTrace });
    }
    catch (Exception ex)
    {
        return this.Problem(ex.Message, "WorkingList/DownloadAndSaveWorkingList", 500);
    }
}

WorkingListAppService
收集图像文件路径(在 Azure 文件共享上)并将其传输到特定服务。该服务将获取图像字节内容,将其作为条目写入直接构建到响应流中的
zipArchive
。此方法用于避免将文件写入磁盘或将其完全加载到内存中:

public async Task<Stream> CreateImageZipFileStream(List<Image> image, string user, CancellationToken token, Stream stream, bool createTreeDirectory = true, bool createMetadataFile = false)
{
    // In this case, stream is Response.BodyWriter.AsStream()
    using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, true))
    {
        foreach (var image in images)
        {
            await AddImageToArchive(archive, createTreeDirectory, createMetadataFile, image, token);
        }
    }

    // Irrelevant here
    return stream;
}

private async Task AddImageToArchive(ZipArchive archive, bool createTreeDirectory, bool createMetadata, Image imageItem, CancellationToken token)
{
    string entryName = imageItem.UserFriendlyName + Constantes.IMAGE_EXTENSION;

    if (createTreeDirectory)
    {
        // Change the entryName to add a subfolder in the archive
        [...]
    }
    
    var entry = archive.CreateEntry(entryName, CompressionLevel.NoCompression);

    using (var entryStream = entry.Open())
    {
        try
        {
            // This methods get the file bytes from an azure fileshare and write it to entryStream
            await this._imageFileShareClient.GetFileContent(imageMaterialFolder + "/" + imageItem.ApplicationCamera.SiteRef.SiteName, imageItem.FilePath, entryStream);
        }
        catch
        {
            try
            {
                entryStream.Position = 0;
                // This methods get a fallback file bytes from an azure fileshare and write it to entryStream
                await this._imageFileShareClient.GetFileContent(fisheyedImageFolder + "/" + imageItem.ApplicationCamera.SiteRef.SiteName, imageItem.FilePath, entryStream);
            }
            catch
            {
                this._logger.LogError("An image requested for download was missing from fileshare. ImageId: {0}", imageItem.Id);
                // Do not throw on error, Ignore missing image and continue
            }
        }
    }
}

更多信息:请求已完全完成,数据已传输到客户端:在控制台的网络选项卡中,请求大小是文件的大小。

我们尝试了

arraybuffer
blob
请求responseType,结果相同。

我们尝试了

application/zip
application/octet-stream
作为 sesponse
Content-Type

库文件保存程序也遇到同样的问题,事件主体为空。

问题提醒

当 API 到达

Response.CompleteAsync()
时,客户端将获取键入
Response
的事件。因此,Angular 应用程序尝试访问为空的响应正文。这是主要问题。

另一个可以解决该问题的附属问题:有没有办法避免在内存客户端加载整个文件?

angular asp.net-core-webapi
1个回答
0
投票

感谢@browsermator的指示,我设法让导航器处理下载。

主要问题是身份验证,我使用令牌绕过了身份验证,以允许用户下次下载。

对于 POST,我生成这样的表单:

<form ngNoForm action="{{baseUrl + 'WorkingList/DownloadWorkingList/' +createFileTree + '/'+ createMetadata + '/' + token}}" method="post">
  <input *ngFor="let imageId of workingList.imageIds; let index = index" type="hidden" name="imageIds[]" [(ngModel)]="workingList.imageIds[index]"/>
  <button type="submit" class="btn btn-primary" rippleEffect>
    {{ "DOWNLOAD_MODAL.CONFIRM" | translate }}
  </button>
</form>

对于 GET(不是问题的一部分),我以与表单提交 url 相同的方式生成 URL,只需添加工作列表 Id 作为参数。

服务器端我添加了对查询url中添加的令牌的检查,并在对象参数上添加了

[FromForm]

这允许导航器将响应作为文件下载来处理,并让他处理资源。

这可能不是最好的(身份验证是通过的,用户身份验证必须手动处理),但它解决了我的问题,我们可以处理这种授权方法。

PS:我没有在控制器中使用 File() 方法,因为它关闭流太快,无法构建存档

© www.soinside.com 2019 - 2024. All rights reserved.