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); } }
问题根源及解决方案
核心问题分析
- 流已被耗尽并释放:你在
using块中将远程流的内容全部写入到服务器本地文件后,流的读取指针已经移到末尾,且using块结束后流会被自动关闭,此时返回给FileDownloadResult的是一个空的、已释放的流,浏览器无法获取有效文件内容。 - 多余的本地文件写入:不需要先将文件保存到服务器本地,直接将远程流转发给客户端即可,既节省服务器磁盘空间,又避免流的重复处理问题。
修改后的流式下载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
相关产品推荐
相关产品推荐

