当前位置: 首页 > news >正文

大文件分片上传完整案例

大文件分片上传:完整前后端代码


前端代码

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>大文件上传</title></head><body><inputtype="file"id="fileInput"/><buttononclick="upload()">上传</button><divid="progress"></div><script>constCHUNK_SIZE=5*1024*1024;// 5M 一片asyncfunctionupload(){constfile=document.getElementById('fileInput').files[0];if(!file)return;consttotalChunks=Math.ceil(file.size/CHUNK_SIZE);constfileId=Date.now()+'-'+Math.random().toString(36).substring(2);// 计算文件的 MD5(秒传/校验用,大文件建议用 SparkMD5 增量计算)// 这里省略 MD5 计算,实际可以加上// 逐片上传说到底就是 for 循环for(leti=0;i<totalChunks;i++){conststart=i*CHUNK_SIZE;constend=Math.min(start+CHUNK_SIZE,file.size);constchunk=file.slice(start,end);constformData=newFormData();formData.append('chunk',chunk);formData.append('fileId',fileId);formData.append('chunkIndex',i);formData.append('totalChunks',totalChunks);formData.append('fileName',file.name);// 上传这一片constresp=awaitfetch('/upload/chunk',{method:'POST',body:formData});constresult=awaitresp.json();if(!result.success){document.getElementById('progress').textContent='上传失败,分片 '+i;return;}// 更新进度constpct=Math.round(((i+1)/totalChunks)*100);document.getElementById('progress').textContent=pct+'%';}// 所有分片上传完成,通知后端合并constresp=awaitfetch('/upload/merge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({fileId,fileName:file.name,totalChunks})});constresult=awaitresp.json();alert('上传完成:'+result.filePath);}</script></body></html>

带并发控制的分片上传

如果一个个上传太慢,可以并发上传,但要注意控制并发数,不然浏览器会把连接占满。

<script>asyncfunctionuploadWithConcurrency(file,maxConcurrent=3){consttotalChunks=Math.ceil(file.size/CHUNK_SIZE);constfileId=Date.now()+'-'+Math.random().toString(36).substring(2);// 把所有分片信息准备好consttasks=[];for(leti=0;i<totalChunks;i++){tasks.push(i);}letcompleted=0;// 控制并发:一次只跑 maxConcurrent 个asyncfunctionworker(){while(tasks.length>0){consti=tasks.shift();conststart=i*CHUNK_SIZE;constend=Math.min(start+CHUNK_SIZE,file.size);constchunk=file.slice(start,end);constformData=newFormData();formData.append('chunk',chunk);formData.append('fileId',fileId);formData.append('chunkIndex',i);formData.append('totalChunks',totalChunks);formData.append('fileName',file.name);constresp=awaitfetch('/upload/chunk',{method:'POST',body:formData});constresult=awaitresp.json();if(!result.success)thrownewError('分片 '+i+' 上传失败');completed++;constpct=Math.round((completed/totalChunks)*100);document.getElementById('progress').textContent=pct+'%';}}// 启动 maxConcurrent 个 workerconstworkers=[];for(leti=0;i<maxConcurrent;i++){workers.push(worker());}awaitPromise.all(workers);// 合并constresp=awaitfetch('/upload/merge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({fileId,fileName:file.name,totalChunks})});returnawaitresp.json();}</script>

后端代码

Controller 层

@RestController@RequestMapping("/upload")publicclassUploadController{@AutowiredprivateChunkUploadServicechunkUploadService;/** * 接收一个分片 */@PostMapping("/chunk")publicResultuploadChunk(@RequestParam("chunk")MultipartFilechunk,@RequestParam("fileId")StringfileId,@RequestParam("chunkIndex")intchunkIndex,@RequestParam("totalChunks")inttotalChunks,@RequestParam("fileName")StringfileName)throwsIOException{chunkUploadService.saveChunk(fileId,chunkIndex,chunk);returnResult.success();}/** * 合并所有分片 */@PostMapping("/merge")publicResultmergeChunks(@RequestBodyMergeRequestrequest)throwsIOException{StringfilePath=chunkUploadService.merge(request.getFileId(),request.getFileName(),request.getTotalChunks());returnResult.success(filePath);}}

分片上传服务

@ServicepublicclassChunkUploadService{/** 临时分片存放目录 */privatestaticfinalStringCHUNK_DIR="/data/uploads/chunks/";/** 合并后的文件存放目录 */privatestaticfinalStringDEST_DIR="/data/uploads/files/";/** * 保存一个分片到临时目录 */publicvoidsaveChunk(StringfileId,intchunkIndex,MultipartFilechunk)throwsIOException{// 每个文件一个文件夹,存放它的所有分片PathchunkDir=Paths.get(CHUNK_DIR,fileId);Files.createDirectories(chunkDir);// 分片文件命名:0、1、2、3...PathchunkFile=chunkDir.resolve(String.valueOf(chunkIndex));try(FileChannelout=FileChannel.open(chunkFile,StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING);FileChannelin=(FileChannel)chunk.getInputStream().getChannel()){longtransferred=0;longfileSize=in.size();while(transferred<fileSize){transferred+=in.transferTo(transferred,fileSize-transferred,out);}}}/** * 合并所有分片 */publicStringmerge(StringfileId,StringfileName,inttotalChunks)throwsIOException{PathchunkDir=Paths.get(CHUNK_DIR,fileId);PathdestFile=Paths.get(DEST_DIR,System.currentTimeMillis()+"_"+fileName);Files.createDirectories(destFile.getParent());try(FileChannelout=FileChannel.open(destFile,StandardOpenOption.WRITE,StandardOpenOption.CREATE,StandardOpenOption.TRUNCATE_EXISTING)){// 按照分片顺序,一个一个写进去for(inti=0;i<totalChunks;i++){PathchunkFile=chunkDir.resolve(String.valueOf(i));if(!Files.exists(chunkFile)){thrownewIOException("分片丢失:"+i);}try(FileChannelin=FileChannel.open(chunkFile,StandardOpenOption.READ)){longtransferred=0;longfileSize=in.size();while(transferred<fileSize){transferred+=in.transferTo(transferred,fileSize-transferred,out);}}}}// 合并完删除临时分片目录deleteChunkDir(chunkDir);returndestFile.toString();}privatevoiddeleteChunkDir(PathchunkDir)throwsIOException{try(Stream<Path>files=Files.list(chunkDir)){files.forEach(path->{try{Files.deleteIfExists(path);}catch(IOExceptionignored){}});}Files.deleteIfExists(chunkDir);}}

DTO

@DatapublicclassMergeRequest{privateStringfileId;privateStringfileName;privateinttotalChunks;}@DatapublicclassResult{privatebooleansuccess=true;privateStringfilePath;publicstaticResultsuccess(){returnnewResult();}publicstaticResultsuccess(StringfilePath){Resultr=newResult();r.filePath=filePath;returnr;}}

用到的关键点

前端

  • file.slice(start, end)— 切分文件,不占额外内存
  • FormData— 传二进制分片,不需要 Base64 编码
  • 并发控制 — 用 worker 模式限制并发数,不要一次性全发出去

后端

  • FileChannel.transferTo— 零拷贝写分片文件和合并
  • 临时分片以fileId/分片序号组织,天然有序
  • 合并完清理临时目录

容错

实际生产还需要补充:

  • 断点续传— 上传前先请求/upload/check?fileId=xxx,后端返回已收到的分片列表,前端跳过这些分片
  • MD5 校验— 前端计算文件 MD5,合并后后端校验是否一致
  • 超时清理— 定时任务清理超过一定时间未合并的临时分片目录
  • 分片大小自适应— 根据网络情况动态调整分片大小(但这个一般不需要,固定 5M 就挺好)
http://www.jsqmd.com/news/1125435/

相关文章:

  • 网页自动化实战指南:从零构建高效工作流
  • 苏州本地GEO获客标杆!环境工程企业AI全域收录破5.2万条
  • 【学生调研报告】网上银行安全架构与安全方案研究
  • 从零构建系统工具:先写验收脚本,再补漂亮交互
  • 无货源自动拍单发货软件靠谱吗?新手先看货源关联和规格匹配一件代发工具教程解析
  • 课堂教学PPT模板推荐哪家?这6个平台教师亲测可用
  • 来博客园的基本是写程序的,好像是废话,缩小点范围,来这里起嘛证明,大家都想学习进步,都是同道中的同道中人。兴趣,往高一点说叫理想,是我们共同的动力,从上一文中再次得到印证。
  • AI编程代理Codex:从安装配置到项目实战的完整指南
  • 基于SpringBoot智慧房屋租赁管理系统的设计与实现任务书
  • 【Aspose-CAD for Java】DWG转PDF实战:精准控制布局与图层,告别空白与错位
  • 基于SpringBoot前后端分离的宠物服务平台开发任务书
  • 2025 全国高联一试 A 卷
  • 五大神经网络核心原理与实战:从CNN到GAN的直观理解与代码实现
  • 从离线分析到实时对话:JoyAI-VL-Interaction如何重塑视频AI交互范式
  • 终极ComfyUI TensorRT插件指南:3-10倍AI绘画加速,释放你的RTX显卡潜能
  • 自动扩缩容:3 种策略的适用场景
  • REACTOS RtlGetVersion 函数实现分析
  • Oracle数据库
  • 终极指南:如何用AI让Monika与你自由对话 - MonikA.I模组完全教程
  • 解决Ant发送邮件显示HTML源码问题:MIME类型配置详解
  • 三菱FX3U PLC运动轴控制与伺服调试实战
  • 如何永久保存微信聊天记录:你的数字记忆完全指南
  • Claude 全面解析:从基础原理到实战应用指南
  • 王千源惊喜亮相HYROX杭州站 不止是演员,更是运动“源”
  • PDF批量签章工具 V5.5 骑缝章智能分割 批量盖章 下载
  • ComfyUI Desktop 实例进入后一直loading的问题解决
  • WPF Multi-Touch 开发:Windows 7 安装多点触屏模拟器
  • 数字孪生助力制造业仿真优化全链路路径
  • STM32 数控电源 PCB 布局 5 要点:从 XL6019 布线到 INA180 抗干扰
  • 设计 Token 语义化:不要把颜色命名成 blue-500 就结束