断点续传(全栈)
断点续传的核心思路是不要把整个大文件一次性传完而是切成很多小块每块单独上传服务器记住“哪些块已经收到了”断了以后只补传缺的块。下面按你项目里的实现把完整过程讲一遍。一、为什么原来不行你原来的接口是这样的也就是一次 POST把整个文件塞进FormData。问题在于传 2GB 文件可能要几分钟中途网络断了、浏览器关了、服务器超时了 → 这次上传全作废只能从头再来所以断点续传必须改成 “分片 记录进度 最后合并”。二、整体流程3 步sequenceDiagram participant 浏览器 participant 后端 participant 磁盘 浏览器-浏览器: 1. 选文件生成 uploadId 浏览器-后端: 2. GET /status查已传分片 后端-磁盘: 看 .chunks/uploadId/ 里有哪些分片 后端--浏览器: 返回 [0,1,2] 已上传 loop 每个缺失分片 浏览器-后端: 3. POST /chunk传一块 5MB 后端-磁盘: 保存为 0、1、2... end 浏览器-后端: 4. POST /merge合并 后端-磁盘: 按顺序拼成完整文件 后端-磁盘: 删除临时分片 后端-数据库: 写入 FileTwo 记录 后端--浏览器: 上传成功可以把它理解成先问服务器“我传到哪了” → 只传没传完的 → 全部齐了再拼成完整文件三、前端做了什么1. 给每个文件生成唯一 IDuploadId// 用「文件名 大小 最后修改时间」算 SHA-256 const raw ${file.name}|${file.size}|${file.lastModified}; const digest await crypto.subtle.digest(SHA-256, ...);作用同一个文件不管今天传还是明天传uploadId都一样。服务器就能认出“这是同一次上传任务”。注意不是按文件内容 hash而是按元信息。内容变了但名字/大小/时间不变会当成同一次上传。对你这个场景一般够用。2. 把文件切成 5MB 一片const CHUNK_SIZE 5 * 1024 * 1024; // 5MB const totalChunks Math.ceil(file.size / CHUNK_SIZE);例如 23MB 文件 → 5 片[0,1,2,3,4]。用file.slice(start, end)从本地文件里切出每一块不用把整个文件读进内存。3. 先查状态断点的关键GET /duandianxuchuan/status?uploadIdxxxtotalChunks5 → 返回 data: [0, 1, 2] // 表示 0、1、2 已经传过了前端用Set存已传分片上传时for (let i 0; i totalChunks; i) { if (!uploadedChunks.has(i)) { // 只传缺失的分片 await uploadChunk(uploadId, i, file.slice(...)); } }这就是“断点续传”传到一半断了 → 重新选同一个文件 → status 告诉你 0、1、2 已有 → 只传 3、4。4. 全部传完后请求合并POST /duandianxuchuan/merge uploadId, fileName, totalChunks, folderId合并成功后文件才真正出现在文件列表里。5. 进度条怎么来的每传完一片就算一下percent (chunkIndex 1) / totalChunks * 100所以进度是按分片数量走的不是精确字节级但体验上够用。四、后端做了什么1. 临时分片存哪/opt/resource/file/hls2/.chunks/{uploadId}/ ├── 0 ← 第 0 片 ├── 1 ← 第 1 片 ├── 2 └── ...uploadId会先做安全校验防止路径穿越。2. status 接口遍历0到totalChunks-1看对应文件是否存在存在就加入uploaded列表返回。3. chunk 接口收到一片后Path chunkPath chunkDir.resolve(String.valueOf(chunkIndex)); if (!Files.exists(chunkPath)) { Files.copy(chunk.getInputStream(), chunkPath); }已存在就跳过 → 同一片重复传也不会出错幂等。4. merge 接口最关键检查0 ~ totalChunks-1是否都在按顺序写入最终文件try (OutputStream out Files.newOutputStream(fileSavePath)) { for (int i 0; i totalChunks; i) { Files.copy(chunkDir.resolve(String.valueOf(i)), out); } }删除.chunks/{uploadId}/临时目录像原来一样写入FileTwo表文件名、路径、大小、folderId 等五、举个具体例子假设你要传一个 12MB 的课件.pdf步骤发生了什么1浏览器算出uploadId a1b2c3...2切成 3 片0(5MB)、1(5MB)、2(2MB)3传完 0、1 后网络断了4你重新选课件.pdf上传5status 返回[0, 1]6只传分片 27merge → 生成/opt/resource/file/hls2/课件.pdf8数据库多一条记录页面刷新后能看到六、和原来接口的对比原来upload_file现在断点续传传法一次传整个文件5MB 一片一片传断了怎么办从头来接着传接口数量1 个3 个status / chunk / merge服务器临时文件无.chunks/目录入库时机传完即入库merge 成功后才入库七、几个细节多文件前端是逐个文件走完整流程每个文件有自己的uploadId。分片大小前后端都是 5MB必须一致。“已存在”检查在 merge 时做不是 chunk 时做。临时分片清理merge 成功后会删如果 merge 前一直失败.chunks会留着方便下次续传也可以以后加定时清理。八、一句话总结前端切片 后端按片存 查进度跳过已传 最后合并入库如果你愿意我还可以单独画一张「用户点上传按钮后浏览器内部函数怎么一层层调用」的流程图或者讲「如果 merge 时少了一片会怎么办」。

相关新闻