文件上传使用分片,文件下载使用流式方案,同时还要实现进度条功能

news/2025/11/11 4:41:29/文章来源:https://www.cnblogs.com/juyss/p/19208738

好的,这是一个非常经典和完整的文件传输方案。我们将采用业界公认的最佳实践:

  • 文件上传前端分片 + 后端记录与合并,实现进度条和断点续传。
  • 文件下载后端流式响应 + 前端读取流,实现平滑进度条。
    这套组合拳能完美处理从小文件到几十GB大文件的各种场景。

一、 文件上传:分片上传 + 进度条

核心思路:将大文件在浏览器端切成多个小块,并发上传,后端接收这些小块并在全部到齐后合并成一个完整文件。

1. 前端实现(JavaScript)

前端是进度计算和任务调度的核心。

<!-- 简单的HTML结构 -->
<input type="file" id="fileInput" />
<button onclick="uploadFile()">上传</button>
<progress id="uploadProgress" value="0" max="100"></progress>
<span id="progressText">0%</span>
async function uploadFile() {const fileInput = document.getElementById('fileInput');const file = fileInput.files[0];if (!file) return;const CHUNK_SIZE = 5 * 1024 * 1024; // 每片5MBconst totalChunks = Math.ceil(file.size / CHUNK_SIZE);let uploadedChunks = 0;// 1. 生成文件唯一标识(用于后端区分不同文件)const fileHash = await calculateFileHash(file); // 你需要实现一个hash计算函数,如spark-md5// 2. 并发上传所有分片const uploadPromises = [];for (let i = 0; i < totalChunks; i++) {const start = i * CHUNK_SIZE;const end = Math.min(file.size, start + CHUNK_SIZE);const chunk = file.slice(start, end);const formData = new FormData();formData.append('file', chunk);formData.append('fileHash', fileHash);formData.append('chunkIndex', i);formData.append('totalChunks', totalChunks);formData.append('fileName', file.name);const uploadPromise = axios.post('/api/upload/chunk', formData).then(() => {uploadedChunks++;// 3. 每完成一个分片,更新进度const progress = (uploadedChunks / totalChunks) * 100;document.getElementById('uploadProgress').value = progress;document.getElementById('progressText').innerText = `${progress.toFixed(2)}%`;});uploadPromises.push(uploadPromise);}try {// 4. 等待所有分片上传完成await Promise.all(uploadPromises);// 5. 通知后端合并文件await axios.post('/api/upload/merge', {fileHash: fileHash,fileName: file.name,totalChunks: totalChunks});alert('文件上传成功!');} catch (error) {console.error('上传失败:', error);alert('上传失败,请重试!');}
}
// 辅助函数:使用spark-md5库计算文件hash
async function calculateFileHash(file) {return new Promise((resolve, reject) => {const spark = new SparkMD5.ArrayBuffer();const reader = new FileReader();const chunkSize = 2 * 1024 * 1024; // 2MBlet currentChunk = 0;const chunks = Math.ceil(file.size / chunkSize);reader.onload = e => {spark.append(e.target.result);currentChunk++;if (currentChunk < chunks) {loadNext();} else {resolve(spark.end());}};reader.onerror = reject;function loadNext() {const start = currentChunk * chunkSize;const end = Math.min(file.size, start + chunkSize);reader.readAsArrayBuffer(file.slice(start, end));}loadNext();});
}

2. 后端实现(Spring Boot)

后端负责接收、存储和合并。

import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.nio.file.*;
import java.util.Map;
@RestController
public class FileUploadController {// 临时分片存储目录private final Path TEMP_DIR = Paths.get("temp-uploads");// 最终文件存储目录private final Path FINAL_DIR = Paths.get("uploads");public FileUploadController() throws IOException {Files.createDirectories(TEMP_DIR);Files.createDirectories(FINAL_DIR);}/*** 接收文件分片*/@PostMapping("/api/upload/chunk")public ResponseEntity<?> uploadChunk(@RequestParam("file") MultipartFile file,@RequestParam("fileHash") String fileHash,@RequestParam("chunkIndex") int chunkIndex) {try {// 使用 fileHash-chunkIndex 作为临时文件名,确保唯一性和顺序Path chunkPath = TEMP_DIR.resolve(fileHash + "-" + chunkIndex);file.transferTo(chunkPath);return ResponseEntity.ok().build();} catch (IOException e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("分片上传失败");}}/*** 合并所有分片*/@PostMapping("/api/upload/merge")public ResponseEntity<?> mergeFile(@RequestBody Map<String, String> request) {String fileHash = request.get("fileHash");String fileName = request.get("fileName");int totalChunks = Integer.parseInt(request.get("totalChunks"));Path targetFile = FINAL_DIR.resolve(fileName);try (FileOutputStream fos = new FileOutputStream(targetFile.toFile())) {// 按顺序读取并写入所有分片for (int i = 0; i < totalChunks; i++) {Path chunkFile = TEMP_DIR.resolve(fileHash + "-" + i);Files.copy(chunkFile, fos);Files.delete(chunkFile); // 合并后删除临时分片}return ResponseEntity.ok("文件合并成功");} catch (IOException e) {return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("文件合并失败");}}
}

二、 文件下载:流式响应 + 进度条

核心思路:后端不将文件读入内存,而是以流的形式写入HTTP响应。前端通过fetchReadableStream实时接收数据并计算进度。

1. 后端实现(Spring Boot)

使用StreamingResponseBody可以实现完美的流式下载,内存占用极低。

import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@RestController
public class FileDownloadController {private final Path FILE_DIR = Paths.get("uploads");@GetMapping("/api/download")public ResponseEntity<StreamingResponseBody> downloadFile(@RequestParam String fileName) {Path filePath = FILE_DIR.resolve(fileName).normalize();if (!Files.exists(filePath) || !Files.isReadable(filePath)) {return ResponseEntity.notFound().build();}StreamingResponseBody stream = outputStream -> {try (InputStream inputStream = Files.newInputStream(filePath)) {byte[] buffer = new byte[4096]; // 4KB bufferint bytesRead;while ((bytesRead = inputStream.read(buffer)) != -1) {outputStream.write(buffer, 0, bytesRead);}} catch (IOException e) {// 可以在这里记录日志}};return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"").body(stream);}
}

2. 前端实现(JavaScript)

前端通过fetch获取响应流,并实时计算下载进度。

<!-- 简单的HTML结构 -->
<button onclick="downloadFile('your-large-file.zip')">下载文件</button>
<progress id="downloadProgress" value="0" max="100"></progress>
<span id="downloadProgressText">0%</span>
async function downloadFile(fileName) {try {const response = await fetch(`/api/download?fileName=${encodeURIComponent(fileName)}`);if (!response.ok) {throw new Error(`下载失败: ${response.statusText}`);}// 1. 从响应头获取文件总大小const contentLength = response.headers.get('Content-Length');const totalBytes = parseInt(contentLength, 10);let receivedBytes = 0;// 2. 获取响应体流const reader = response.body.getReader();const chunks = [];while (true) {const { done, value } = await reader.read();if (done) break;chunks.push(value); // value 是一个 Uint8ArrayreceivedBytes += value.length;// 3. 计算并更新进度if (totalBytes) {const progress = (receivedBytes / totalBytes) * 100;document.getElementById('downloadProgress').value = progress;document.getElementById('downloadProgressText').innerText = `${progress.toFixed(2)}%`;}}// 4. 所有数据接收完毕,合并并触发浏览器下载const blob = new Blob(chunks);const downloadUrl = URL.createObjectURL(blob);const a = document.createElement('a');a.href = downloadUrl;a.download = fileName;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(downloadUrl); // 释放内存} catch (error) {console.error(error);alert('下载出错!');}
}

总结

这套方案的优势非常明显:

  • 高性能:无论是上传还是下载,都避免了在内存中处理整个大文件,内存占用极低。
  • 用户体验好:实时、平滑的进度条,让用户清楚地知道传输状态。
  • 高可靠性:上传支持断点续传(前端记录已上传分片即可),下载也易于扩展支持断点续传(后端需支持Range请求,StreamingResponseBody方案已天然兼容)。
  • 可扩展性强:这是处理大文件传输的工业级标准方案,可以在此基础上轻松添加更多功能,如秒传(通过文件hash判断文件是否存在)、并发控制等。

以上内容由AI生成,仅供参考和借鉴

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/961945.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HeavenHell

Heaven&Hell只要是我能做的,我什么都愿意做。香樟枯叶有淡淡的铁锈味。然后我感觉高兴。我不喜欢甜腻的薄荷的香味,我喜欢好闻的味道。 一事无成,我想是的。我甚至失去死在十八岁前夜的勇气。我越来越冗长了。 …

Why can people actually only speak one language

its the language of themselves. USA CHN, UK are all bad.

基于MATLAB的3-PUU并联机构工作空间仿真分析

1.课题概述 3-PUU 并联机构是一种典型的空间并联机构,在机器人、精密定位设备等诸多领域有着广泛的应用。对其工作空间进行准确分析,有助于了解机构的运动范围和性能,从而为机构的设计、控制和应用提供重要依据。 2…

基于氢氧燃料电池的分布式三相电力系统Simulink建模与仿真

1.课题概述 基于氢氧燃料电池的分布式三相电力系统Simulink建模与仿真,仿真输出燃料电池中氢氧元素含量变化以及生成的H2O变化情况。 2.系统仿真结果3.核心程序与模型 版本:MATLAB2022a4.系统原理简介 氢氧燃料电池原…

sunk cost

If there were no utopia or paradise in the world all we need is just sunk cost. although somehow I indeed speak Chinese unwillingly.

英皇热水器售后服务电话4009968065

英皇热水器推出24小时售后客服受理中心(2025更新上线) 英皇热水器专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,英皇热水器作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导致机器…

巧夫人油烟机售后服务电话4009968065

巧夫人油烟机推出24小时售后客服受理中心(2025更新上线) 巧夫人油烟机专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,巧夫人油烟机作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导…

爱多集成灶售后服务电话4009968065

爱多集成灶推出24小时售后客服受理中心(2025更新上线) 爱多集成灶专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,爱多集成灶作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导致机器…

大宇mini壁挂洗衣机售后服务电话4009968065

大宇mini壁挂洗衣机推出24小时售后客服受理中心(2025更新上线) 大宇mini壁挂洗衣机专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,大宇mini壁挂洗衣机作为家庭温暖的守护者,其稳定运行至关重要。但长期使…

凯昆空气能售后服务电话4009968065

凯昆空气能推出24小时售后客服受理中心(2025更新上线) 凯昆空气能专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,凯昆空气能作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导致机器…

新宝油烟机售后服务电话4009968065

新宝油烟机推出24小时售后客服受理中心(2025更新上线) 新宝油烟机专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,新宝油烟机作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导致机器…

桑乐太阳能售后服务电话4009968065

桑乐太阳能推出24小时售后客服受理中心(2025更新上线) 桑乐太阳能专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,桑乐太阳能作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导致机器…

联合丽家集成灶售后服务电话4009968065

联合丽家集成灶推出24小时售后客服受理中心(2025更新上线) 联合丽家集成灶专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,联合丽家集成灶作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题…

美满热水器售后服务电话4009968065

美满热水器推出24小时售后客服受理中心(2025更新上线) 美满热水器专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,美满热水器作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导致机器…

法帝油烟机售后服务电话4009968065

法帝油烟机推出24小时售后客服受理中心(2025更新上线) 法帝油烟机专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,法帝油烟机作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导致机器…

迪堡保险柜售后服务电话4009968065

迪堡保险柜推出24小时售后客服受理中心(2025更新上线) 迪堡保险柜专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,迪堡保险柜作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导致机器…

和美好太太燃气灶售后服务电话4009968065

和美好太太燃气灶推出24小时售后客服受理中心(2025更新上线) 和美好太太燃气灶专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,和美好太太燃气灶作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各…

顾阳防盗门售后服务电话4009968065

顾阳防盗门推出24小时售后客服受理中心(2025更新上线) 顾阳防盗门专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,顾阳防盗门作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导致机器…

贵匠电暖炉售后服务电话4009968065

贵匠电暖炉推出24小时售后客服受理中心(2025更新上线) 贵匠电暖炉专业售后维修服务电话:4009968065让温暖无忧回归 冬季来临,贵匠电暖炉作为家庭温暖的守护者,其稳定运行至关重要。但长期使用后各种问题可能导致机器…