news 2026/1/9 9:32:12

大文件上传

作者头像

张小明

前端开发工程师

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进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/25 17:42:51

微服务架构治理新范式:AI驱动的依赖关系智能解析与优化

在微服务架构日益普及的今天&#xff0c;依赖关系管理已成为制约系统稳定性和开发效率的关键因素。行业数据显示&#xff0c;超过75%的微服务项目在运行两年后会出现严重的依赖混乱问题&#xff0c;导致系统可维护性急剧下降。随着服务规模扩大&#xff0c;传统的依赖管理方式已…

作者头像 李华
网站建设 2025/12/25 17:38:27

打造完全本地隐私的AI助理:Obsidian+Ollama+Qwen 3构建个人RAG知识库

本文介绍如何使用本地OllamaQwen 3模型结合Obsidian构建完全隐私保护的RAG知识库。作者解释了RAG技术原理&#xff0c;将Obsidian笔记向量化存储到本地ChromaDB&#xff0c;实现基于个人知识的智能问答。开发了名为MyGPT的本地应用&#xff0c;解决了云端AI的隐私泄露风险和网络…

作者头像 李华
网站建设 2026/1/8 11:06:02

【稀缺资源】Open-AutoGLM级AI仅此6款:掌握这4个判断标准避免选错

第一章&#xff1a;Open-AutoGLM类似的ai有哪些?在人工智能领域&#xff0c;尤其是面向自动化代码生成与自然语言理解任务中&#xff0c;Open-AutoGLM 作为一种结合大语言模型与图学习的开源框架&#xff0c;激发了众多类似系统的研发。这些系统在架构设计、应用场景和扩展能力…

作者头像 李华
网站建设 2026/1/3 6:05:44

ruoyi集成 camunda 实现审批驳回

驳回是指审批人或司法机关对提交的申请或请求进行审查后&#xff0c;认为其不符合要求或无法成立&#xff0c;从而作出的不予同意、拒绝其通过的决定&#xff0c;该决定通常会导致流程回退或申请被否定。 演示地址ruoyiflow驳回功能演示 测试账号信息: 账号: ry 密码: ry2025账…

作者头像 李华