You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Web API实现2GB+大文件下载:流式下载后浏览器无下载提示问题

问题描述

我在Angular网站中调用Web API实现文件下载,用户点击链接后网站显示下载进度,完成后浏览器弹出下载窗口,该流程在文件小于2GB时正常运行。经排查,大于2GB的文件无法下载是因为File.ReadAllBytes()存在限制。参考相关资料后,我尝试用HttpClient实现流式分块下载,本地运行时文件可完整保存到服务器的tempFile路径(C:\NoSyncData\files\productABC\),但浏览器未弹出下载窗口,文件也未出现在客户端下载文件夹中。我知道API无法指定客户端文件保存路径,想咨询如何让浏览器正常触发下载流程,将文件保存到客户端下载文件夹。


原有可用代码

Get端点

public IHttpActionResult Get(string data)
{
    return Functions.GetFile(data, Request);
}

GetFile()方法

public static IHttpActionResult GetFile(string data, HttpRequestMessage request)
{
    // 从加密字符串获取数据
    SecuredString securedString = new SecuredString(data);
    Enums.FileActionType fileActionType = securedString["fileActionType"].ToEnum<Enums.FileActionType>();
    string customerCode = securedString["customerCode"];
    string fileName = securedString["fileName"];
    string filePathWithName = securedString["filePathWithName"];
    string fileFullPath = $"{Constants.BasePath}";
    string tempFile = $"{Constants.TempFolderPath}{fileName}";

    try
    {
        // 根据下载类型获取文件夹结构
        switch (fileActionType)
        {
            case Enums.FileActionType.Video:
                string videoFilePath = securedString["filePath"];
                fileFullPath += $"{videoFilePath}{fileName}";
                break;

            case Enums.FileActionType.CBI:
            case Enums.FileActionType.Manual:
            case Enums.FileActionType.Report:
                fileFullPath += filePathWithName;
                break;

            case Enums.FileActionType.ProductDocument:
                // 正常路径从这里获取,为了可读性暂时硬编码
                // string filePath = securedString["filePath"];
                fileFullPath = $"https://localhost:44347/asc/customers/public/files/productABC/{fileName}";
                break;
        }

        // 将文件转换为字节数组
        var dataBytes = File.ReadAllBytes(fileFullPath);

        // 将字节数组加入内存流
        var dataStream = new MemoryStream(dataBytes);

        // 创建结果并返回
        return new FileDownloadResult(dataStream, request, fileName);
    }
    catch (Exception e)
    {
        throw e;
    }
    finally
    {
    }
}

尝试的流式下载Get方法

public async Task<IHttpActionResult> Get(string data)
{
    int bufferSize = 8192;
    string fileName = "productABC_documentation.zip"; // ~119MB
    string tempFile = $@"C:\NoSyncData\files\productABC\{fileName}";         
    string url = $"https://localhost:44347/asc/customers/public/files/productABC/{fileName}";

    try
    {
        using (HttpClient client = new HttpClient())
        using (HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead))
        {
            response.EnsureSuccessStatusCode();

            using (Stream stream = await response.Content.ReadAsStreamAsync(),
                    fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize, true))
            {
                int bytesRead;
                byte[] buffer = new byte[bufferSize];

                while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
                    await fileStream.WriteAsync(buffer, 0, bytesRead);

                return new FileDownloadResult(stream, Request, fileName);
            }
        }
    }
    catch (Exception e)
    {
        // 抛出所有异常的错误信息
        string message = Functions.BuildErrorMessage("下载文件时出错", e);
        throw new Exception("下载文件时出错\n" + message);
    }
    finally
    {
            
    }
}

FileDownloadResult类

public class FileDownloadResult : IHttpActionResult
{
    string fileName;
    Stream fileStream;
    HttpRequestMessage httpRequestMessage;
    HttpResponseMessage httpResponseMessage;

    public FileDownloadResult(Stream data, HttpRequestMessage request, string filename)
    {
        fileStream = data;
        httpRequestMessage = request;
        fileName = filename;
    }

    public Task<HttpResponseMessage> ExecuteAsync(System.Threading.CancellationToken cancellationToken)
    {
        httpResponseMessage = httpRequestMessage.CreateResponse(HttpStatusCode.OK);
        httpResponseMessage.Content = new StreamContent(fileStream);
        httpResponseMessage.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment");
        httpResponseMessage.Content.Headers.ContentDisposition.FileName = fileName;
        httpResponseMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");

        return Task.FromResult(httpResponseMessage);
    }
}

问题根源及解决方案

核心问题分析

  1. 流已被耗尽并释放:你在using块中将远程流的内容全部写入到服务器本地文件后,流的读取指针已经移到末尾,且using块结束后流会被自动关闭,此时返回给FileDownloadResult的是一个空的、已释放的流,浏览器无法获取有效文件内容。
  2. 多余的本地文件写入:不需要先将文件保存到服务器本地,直接将远程流转发给客户端即可,既节省服务器磁盘空间,又避免流的重复处理问题。

修改后的流式下载Get方法

public async Task<IHttpActionResult> Get(string data)
{
    int bufferSize = 8192;
    // 实际场景中请还原从securedString解析fileName的逻辑
    string fileName = "productABC_documentation.zip";
    string url = $"https://localhost:44347/asc/customers/public/files/productABC/{fileName}";

    try
    {
        using (HttpClient client = new HttpClient())
        {
            var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
            response.EnsureSuccessStatusCode();

            // 获取远程响应流,不要用using包裹,交给FileDownloadResult处理释放
            Stream stream = await response.Content.ReadAsStreamAsync();
            
            // 直接将远程流返回给客户端
            return new FileDownloadResult(stream, Request, fileName);
        }
    }
    catch (Exception e)
    {
        string message = Functions.BuildErrorMessage("下载文件时出错", e);
        throw new Exception("下载文件时出错\n" + message);
    }
}

Angular端配合(可选)

如果Angular通过HttpClient发起请求,需将响应类型设置为blob,再手动创建下载链接触发浏览器弹窗:

downloadFile(data: string): void {
  this.http.get(`/api/download?data=${data}`, { responseType: 'blob' })
    .subscribe(blob => {
      const url = window.URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      // 可从响应头的Content-Disposition中解析文件名,替代硬编码
      a.download = 'productABC_documentation.zip'; 
      document.body.appendChild(a);
      a.click();
      window.URL.revokeObjectURL(url);
      document.body.removeChild(a);
    });
}

内容的提问来源于stack exchange,提问作者Robert

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 11:57:15