news 2026/6/15 16:48:50

大文件上传

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大文件上传

大文件上传

前后端配合,前端进行文件切片,计算文件hash,作为与后端协作的唯一凭证,标明是哪个文件。

上传的切片信息需包含4个部分:切片索引,文件hash, 总分片数,分片的内容 {index,hash, total, chuck}。

后端API提供:

  • /check 文件是否上传过
  • /uploaded-chunks 已上传的分片列表(后端以chuck的索引存储)
  • /upload-chunk 上传分片
  • /merge 合并分片

文件切片

constCHUNK_SIZE=2*1024*1024;// 2MB// 将文件切片成多个 Blob// 返回 Blob 数组,每个 Blob 保留原始文件的 typefunctionsliceFile(file,chunkSize=CHUNK_SIZE){constchunks=[];consttotalChunks=Math.ceil(file.size/chunkSize);for(leti=0;i<totalChunks;i++){conststart=i*chunkSize;constend=Math.min(file.size,start+chunkSize);constslicedChunk=file.slice(start,end);// file.slice() 在某些情况下可能不会保留原始文件的 type// 手动创建 Blob 并保留原始文件的 typeconstchunk=newBlob([slicedChunk],{type:file.type||''// 保留原始文件的 type,如果是空字符串也保留});chunks.push(chunk);}returnchunks;}

计算文件hash

// 计算文件hash(使用SparkMD5库)// 增量算法:分块计算,因为计算hash要读取整个文件的内容,一次性读取到内存中,内存会溢出OOM。读取一块计算该块的hash,最后合并。functioncalculateHash(file){returnnewPromise((resolve)=>{constspark=newSparkMD5()// 使用统一的切片函数constchunks=sliceFile(file,CHUNK_SIZE)function_read(index){if(index>=chunks.length){resolve(spark.end())//返回文件hashreturn//读取完成}constblob=chunks[index]if(blob){constfileReader=newFileReader()fileReader.onload=(e)=>{constbytes=e.target.result//读取到的字节数组ArrayBufferspark.append(bytes)_read(index+1)}fileReader.readAsArrayBuffer(blob)}}_read(0)})}

分片上传

// 分片上传asyncfunctionuploadFile(file){if(!file)return;// 计算文件hashconstfileHash=awaitcalculateHash(file);console.log(fileHash)// 检查文件是否已经上传过(秒传)constcheckResp=awaitapiClient.post('/api/check',{hash:fileHash});constcheckResult=checkResp.data;if(checkResult.exists){// 秒传,直接跳转到结果页面alert('文件已存在,秒传成功!');return;}// 获取已上传的分片(断点续传)constuploadedResp=awaitapiClient.post('/api/uploaded-chunks',{hash:fileHash});constuploadedChunks=uploadedResp.data;// 假设返回已上传的分片索引数组// 使用统一的切片函数constblobChunks=sliceFile(file,CHUNK_SIZE);consttotalChunks=blobChunks.length// 将 Blob 数组转换为包含元数据的分片对象数组constchunks=blobChunks.map((chunk,index)=>({index:index,hash:fileHash,chunk:chunk,total:totalChunks}));// 上传分片(过滤已上传的分片)constchunksToUpload=chunks.filter(chunk=>!uploadedChunks.includes(chunk.index));// 控制并发上传数// 建议值:// - 小文件(< 50MB):3-5// - 中等文件(50-500MB):3-6// - 大文件(> 500MB):4-8// 注意:浏览器 HTTP/1.1 每个域名最多 6 个并发连接constconcurrency=Math.min(6,Math.max(3,Math.ceil(totalChunks/10)));// 动态调整,但不超过6// const concurrency = 1console.log(`总分片数:${totalChunks}, 并发数:${concurrency}`);// console.log(uploadChunk(chunks[0]))try{constresults=awaitrunConcurrent(chunksToUpload,concurrency,uploadChunk);// 检查所有分片是否上传成功constfailedChunks=[];results.forEach((result,index)=>{// 如果 result 是错误对象,则认为失败if(resultinstanceofError){console.error(`分片${chunksToUpload[index].index}上传失败:`,result);failedChunks.push({index:chunksToUpload[index].index,error:result});return;}// 如果 result 为空或 undefined,认为失败if(!result){console.error(`分片${chunksToUpload[index].index}上传失败: 返回结果为空`);failedChunks.push({index:chunksToUpload[index].index,error:'返回结果为空'});return;}// 如果后端返回了 success 字段,检查是否为 falseif(result.success===false){console.error(`分片${chunksToUpload[index].index}上传失败:`,result);failedChunks.push({index:chunksToUpload[index].index,error:result});return;}});if(failedChunks.length>0){alert(`${failedChunks.length}个分片上传失败,请重试!`);return;}console.log('所有分片上传成功,开始合并...');// 所有分片上传完成后,通知合并constmergeResp=awaitapiClient.post('/api/merge',{hash:fileHash,total:totalChunks});constmergeResult=mergeResp.data;if(mergeResult.success){alert('上传成功!');// 跳转到结果页面等}else{alert('合并失败,请重试!');}}catch(error){console.error('上传过程中发生错误:',error);alert('上传失败,请重试!');}}// 上传单个分片asyncfunctionuploadChunk(chunk){constformData=newFormData();formData.append('hash',chunk.hash);formData.append('index',chunk.index);formData.append('total',chunk.total);formData.append('chunk',chunk.chunk);constresp=awaitapiClient.post('/api/upload-chunk',formData);returnresp.data;}// 并发控制// tasks: 任务数组(数据)// concurrency: 最大并发数// taskFn: 执行任务的函数asyncfunctionrunConcurrent(tasks,concurrency,taskFn){constresults=[];constexecuting=[];for(consttaskoftasks){// 立即执行任务函数,创建 Promise(任务会立即开始执行)constpromise=Promise.resolve().then(()=>taskFn(task));results.push(promise);// 创建一个清理函数,当任务完成时从 executing 数组中移除constcleanup=promise.finally(()=>{// promise.finally 会在任务完成(成功或失败)后执行// 从 executing 数组中移除这个已完成的任务constindex=executing.indexOf(cleanup);if(index>-1){executing.splice(index,1);}});executing.push(cleanup);// 如果达到并发上限,等待至少一个任务完成// Promise.race 会等待 executing 中任意一个 Promise 完成。阻塞循环if(executing.length>=concurrency){awaitPromise.race(executing);}}// 等待所有任务完成(使用 allSettled 确保即使有失败也能获取所有结果)constsettledResults=awaitPromise.allSettled(results);// 将 allSettled 的结果转换为普通结果或错误returnsettledResults.map((result,index)=>{if(result.status==='fulfilled'){returnresult.value;// 成功的结果}else{returnresult.reason;// 失败的错误对象}});}

后端

constexpress=require('express');constmulter=require('multer');constfs=require('fs');constpath=require('path');constcors=require('cors');constapp=express();app.use(cors());app.use(express.json());//只负责解析 Content-Type: application/json 的请求constUPLOAD_DIR=path.resolve(__dirname,'uploads');// 确保上传目录存在if(!fs.existsSync(UPLOAD_DIR)){fs.mkdirSync(UPLOAD_DIR);}// 存储分片的临时目录constCHUNK_DIR=path.resolve(UPLOAD_DIR,'chunks');if(!fs.existsSync(CHUNK_DIR)){fs.mkdirSync(CHUNK_DIR);}// 检查文件是否存在(秒传)app.post('/api/check',(req,res)=>{const{hash}=req.body;constfilePath=path.resolve(UPLOAD_DIR,hash);if(fs.existsSync(filePath)){res.json({exists:true});}else{res.json({exists:false});}});// 获取已上传的分片列表app.post('/api/uploaded-chunks',(req,res)=>{const{hash}=req.body;constchunkDir=path.resolve(CHUNK_DIR,hash);if(!fs.existsSync(chunkDir)){returnres.json([]);}constchunks=fs.readdirSync(chunkDir);// 假设分片文件名就是索引constuploadedChunks=chunks.map(chunk=>parseInt(chunk));res.json(uploadedChunks);});// 上传分片//multer:只负责解析 带文件的 multipart/form-data 请求constupload=multer({dest:CHUNK_DIR});app.post('/api/upload-chunk',upload.single('chunk'),(req,res)=>{// 检查文件是否成功上传if(!req.file){returnres.status(400).json({success:false,message:'文件上传失败:未检测到文件。请确保前端使用 FormData 发送,且文件字段名为 "chunk"'});}const{hash,index}=req.body;console.log('upload-chunk:',{hash,index,file:req.file.path});// 检查必要参数if(!hash||index===undefined){returnres.status(400).json({success:false,message:'缺少必要参数:hash 或 index'});}constchunkDir=path.resolve(CHUNK_DIR,hash);if(!fs.existsSync(chunkDir)){fs.mkdirSync(chunkDir,{recursive:true});}// 将上传的临时文件移动到对应的分片目录,并以索引命名consttempPath=req.file.path;consttargetPath=path.resolve(chunkDir,index);fs.renameSync(tempPath,targetPath);res.json({success:true});});// 合并分片app.post('/api/merge',async(req,res)=>{const{hash,total}=req.body;constchunkDir=path.resolve(CHUNK_DIR,hash);constfilePath=path.resolve(UPLOAD_DIR,hash);// 检查分片是否全部上传完成constchunks=fs.readdirSync(chunkDir);// console.log(chunks.length)if(chunks.length!==total){returnres.status(400).json({success:false,message:'分片数量不符'});}// 按索引排序分片constsortedChunks=chunks.map(chunk=>parseInt(chunk)).sort((a,b)=>a-b);// 合并分片constwriteStream=fs.createWriteStream(filePath);for(constchunkIndexofsortedChunks){constchunkPath=path.resolve(chunkDir,chunkIndex.toString());constchunkBuffer=fs.readFileSync(chunkPath);writeStream.write(chunkBuffer);fs.unlinkSync(chunkPath);// 删除分片}writeStream.end();// 删除分片目录fs.rmdirSync(chunkDir);// 这里可以调用后续处理日志文件的函数,并返回处理结果res.json({success:true});});app.listen(3000,()=>{console.log('Server is running on port 3000');});
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/15 13:32:56

大理的AI野心藏不住了——风花雪月中千名程序员探讨人工智能

当苍山雪遇上代码雨&#xff0c;当洱海月映照服务器——2025 年12月4日至6日第二届CCF程序员大会暨大理人工智能与应用国际开发者大会在大理圆满落幕。这场由工业级5G 创新应用&#xff08;大理&#xff09;研究院牵头承办的科技盛会&#xff0c;没走“北上广深内卷老路”&…

作者头像 李华
网站建设 2026/6/15 13:35:12

EM 算法 (期望最大化):在迷雾中寻找真相

图解说明&#xff1a; &#x1f4ca; 灰色直方图&#xff1a;代表我们看到的观测数据&#xff08;混在一起&#xff0c;分不清谁是谁&#xff09;。&#x1f535; 蓝色虚线/实线&#xff1a;代表我们猜测的分布 A&#xff08;比如男生身高&#xff09;。&#x1f534; 红色虚线…

作者头像 李华
网站建设 2026/6/15 11:47:45

Java毕设项目:基于springboot的校园快递仓库管理系统的设计与实现(源码+文档,讲解、调试运行,定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/6/15 14:29:58

Open-AutoGLM插件到底怎么用?下载、安装、调试全链路拆解

第一章&#xff1a;智谱Open-AutoGLM下载 Open-AutoGLM 是智谱AI推出的一款面向自动化代码生成与任务处理的开源大模型工具&#xff0c;支持本地部署与二次开发。用户可通过官方仓库获取源码并快速搭建运行环境。 获取源码方式 访问智谱AI官方GitHub组织页面&#xff1a;http…

作者头像 李华
网站建设 2026/6/15 11:46:37

为什么顶尖团队开始转向Open-AutoGLM平替?揭秘背后3大核心技术优势

第一章&#xff1a;Open-AutoGLM类似的ai有哪些?在人工智能与自然语言处理快速发展的背景下&#xff0c;涌现出许多与 Open-AutoGLM 类似的开源或闭源 AI 框架和模型。这些系统通常专注于自动化机器学习任务、代码生成、自然语言理解以及多模态推理。主流替代框架 AutoGPT&…

作者头像 李华
网站建设 2026/6/15 12:17:52

Open-AutoGLM Mac部署秘籍,仅限本周公开的高效配置方案

第一章&#xff1a;Open-AutoGLM Mac部署概述Open-AutoGLM 是一个面向 macOS 平台的自动化大语言模型推理框架&#xff0c;专为本地化部署与高效推理设计。它结合了 AutoGLM 推理引擎与 Apple Silicon 的神经网络加速能力&#xff0c;能够在 M1/M2 系列芯片上实现低延迟、高吞吐…

作者头像 李华