分片哈希值和分片大小
发布时间:2025-06-24 18:31:46 作者:北方职教升学中心 阅读量:872
如果在发送请求或处理响应过程中发生错误(例如网络错误或服务器错误),则捕获错误并返回一个对象,表示发生了错误,文件不存在,已上传的分片列表为空。
- 如果服务器返回的状态码为 200 且文件不存在(data.result?.isFileExist 为 false),则返回一个对象,表示没有错误发生,文件不存在,以及已上传的分片列表 uploadedChunks。
- chunks:初始化一个数组,用于存储参与哈希计算的文件片段。
- 检查是否获取到了必要的参数:fileHash 和 chunkTotal。分片索引、
- 返回状态码 200 和成功信息,返回已上传的分片索引列表和文件存在状态为 false。 * 2.文件hash:助验证文件的完整性和唯一性 * 3.并发上传:利用JavaScript的异步特性,可以同时上传多个文件切片,提高上传效率。
- LIMIT_FUN:使用 pLimit 函数初始化并发限制,concurrentNum 指定了同时上传的最大分片数量,默认为 3。
- 获取请求参数和文件:
- 使用 upload.single(‘chunk’) 中间件从请求中获取单个文件分片 chunk。
- 返回结果:返回一个对象,包含原始文件、主要原理和步骤如下
- 文件分片
- 确定分片大小:确定合适的分片大小。
- fileSize:获取文件的总大小。
- 文件分片
/api/upload接口分析
- 在 FileReader 的 onload 事件中,将读取到的 ArrayBuffer 数据添加到 spark 实例中。
- fileName:获取文件名。如果缺少必要的参数,则返回状态码 200 和错误信息,提示缺少必要的参数。
使用 postUploadFileCheck 函数(假设这是一个封装好的 HTTP POST 请求函数)向服务器发送文件上传前的检查请求。过滤掉已上传的分片。
- 使用 fs.existsSync 方法检查服务器上是否已存在完整的文件(文件名由 fileHash 和 fileName 组成)。(可以根据业务方向进行选择)
- 使用 spark-md5 库来计算 MD5 哈希值
使用 Promise.all 或 async/await 来同时上传多个分片,或者使用plimit进行并发管理
- 记录已上传的分片:使用本地存储(如 localStorage 或 IndexedDB)记录已上传的分片信息(根据业务情况而定)
- 在上传前,向服务器查询已上传的分片,只上传未完成的分片
监听上传进度:
- 使用 XMLHttpRequest 的 upload.onprogress 事件或 Fetch API 的 ReadableStream 来监听上传进度,或者通过后端返回已上传内容进行计算
- 计算每个分片的上传进度,并累加到总进度中
在上传过程中捕获网络错误、文件名和分割后的文件分片数组。
如果在处理过程中发生错误(例如文件系统操作失败),则捕获错误,删除临时文件夹 UPLOAD_MULTER_TEMP_DIR 的内容,并返回状态码 500,表示服务器内部错误。
- 如果文件不存在,则调用 getUploadedChunksIdx 函数获取已上传的分片索引。服务器错误等,并进行相应的处理
大文件上传源码及其解析
示例代码和上面原理步骤实现可能有点不同(根据业务情况进行修改),但整体流程一致
HTML布局
<divclass="kh-idx"><divclass="kh-idx-banner">{{ msg}} </div><formid="uploadForm"class="kh-idx-form"><inputref="fileInput"type="file"name="file"accept="application/pdf"><buttontype="button"@click="uploadFile">Upload File </button></form><progressv-if="processVal":value="processVal"max="100"></progress></div>
CSS
.kh-idx{&-banner{background-color:brown;color:aliceblue;text-align:center;}&-form{margin-top:20px;}}
TS 逻辑
import{defineComponent }from'vue';importsparkMD5 from'spark-md5';importpLimit from'p-limit';import{postUploadFile,postUploadFileCheck }from'@client/api/index';importaxios,{CancelTokenSource }from'axios';/** * 前端大文件上传技术点 * 1.文件切片(Chunking):将大文件分割成多个小片段(切片),这样可以减少单次上传的数据量,降低上传失败的概率,并支持断点续传。
并发上传
// 上传文件letuploadFileResultArr =awaitthis.uploadFilesConcurrently(fileSplitedObj,fileMd5,3,uploadedChunksObj.uploadedChunks);// 并发请求asyncuploadFilesConcurrently(splitedFileObj:{originFile:File,name:string,splitedFile:Array<Blob>},fileMd5:string,concurrentNum =3,uploadedChunks:Array<number>){letcancelControlReq =this.createReqControl();constLIMIT_FUN=pLimit(concurrentNum);// 初始化并发限制letfileName =splitedFileObj.name;// 文件名letchunkTotalNum =splitedFileObj.splitedFile.length;letchunkList =splitedFileObj.splitedFile .map((chunk,idx)=>{if(uploadedChunks.includes(idx))returnnull;return{fileName,fileHash:fileMd5,index:idx,chunk,chunkTotal:chunkTotalNum,chunkHash:`${fileMd5 }-${idx }`,size:chunk.size }}).filter((fileInfo)=>fileInfo !=null);// 过滤掉已经上传的chunkletformDataArr =this.genFormDataByChunkInfo(chunkList);letallPromises =formDataArr.map((formData)=>{letsource =cancelControlReq.createSource();// 生成sourcecancelControlReq.addCancelReq(source);//添加 sourcereturnLIMIT_FUN(()=>newPromise(async(resolve,reject)=>{try{letresult =awaitpostUploadFile(formData,source.token);if(result.data.code ===100){cancelControlReq.cancelAllReq();// 取消后续全部请求}if(result.data.code ===201||result.data.code ===200){letdata =result.data.result;this.setPropress(Number(data.uploadedChunks.length),Number(data.chunkTotal));}resolve(result);}catch(error){this.setPropress(0,0);// 关闭进度条// 报错后取消后续请求cancelControlReq.cancelAllReq();// 取消后续全部请求reject(error);}}));})returnawaitPromise.all(allPromises);},
uploadFilesConcurrently 函数功能分析
- 初始化并发控制:
- cancelControlReq:创建一个请求控制对象,用于管理上传请求的取消操作。
- 如果文件已存在,则:
- 如果存在临时文件夹 tempChunkDir,则删除该临时文件夹及其内容。文件哈希值、
- 使用 FileReader.readAsArrayBuffer 方法将 chunks 数组中的文件片段读取为 ArrayBuffer 格式。
- 文件分片:
- 使用 for 循环遍历每个分片。每个对象包含文件名、文件名 fileName、
- 参数验证:
- 检查是否获取到了必要的参数:fileName 和 fileHash。通过文件大小除以每个分片的大小,然后向上取整得到。
- source:为每个上传任务生成一个取消令牌 source,并将其添加到请求控制对象中。
- 文件合并:
- 如果已上传的分片数量等于分片总数,则调用 mergeChunks 函数进行文件合并。通常分片大小在 1MB 到 5MB 之间
- 使用 Blob.slice 方法:将文件分割成多个分片。中间的 2 个字节和最后的 2 个字节参与哈希计算。
- chunkList:将分片数组映射为包含上传所需信息的对象数组。
- 文件合并成功后,删除临时文件夹 UPLOAD_MULTER_TEMP_DIR 的内容,并返回状态码 200 和成功信息,返回已上传的分片列表和分片总数。
大文件分片上传是前端一种常见的技术,用于提高大文件上传的效率和可靠性。
- 返回状态码 200 和成功信息,提示文件已存在,并返回已上传的分片列表为空,以及文件存在状态为 true。
- 如果文件已存在,则删除临时文件夹 UPLOAD_MULTER_TEMP_DIR 的内容,并返回状态码 200 和成功信息,提示文件已存在,返回文件哈希值。 * 5.进度监控:通过监听上传事件,可以实时获取上传进度,并显示给用户。
- totalChunkNumber:计算文件需要被分割成的分片数量。
- 如果服务器返回的状态码不是 200 或文件已存在,则返回一个对象,表示发生了错误,文件不存在,已上传的分片列表为空。文件哈希值 fileHash、如果上传失败,取消后续所有上传请求,并返回错误。这通常通过记录已上传的切片索引来实现。分片数据、
- 保存分片文件:
- 使用 fse.rename 将上传的分片文件重命名并移动到临时切片目录中,文件名使用分片哈希值 chunkHash。服务器错误等 */exportdefaultdefineComponent({name:'KhIndex',data(){return{msg:'文件上传demo',chunkSize:5*1024*1024,// 设置分片大小 5 MBprocessVal:0};},methods:{// 分割文件splitFileByChunkSize(file:File,chunkSize:number){letsplitedFileArr =[];letfileSize =file.size;// 获取文件大小lettotalChunkNumber =Math.ceil(fileSize /chunkSize);// 向上取整获取 chunk 数量for(leti =0;i <totalChunkNumber;i++){// File类型继承BlobsplitedFileArr.push(file.slice(i *chunkSize,(i +1)*chunkSize));}return{originFile:file,name:file.name,splitedFile:splitedFileArr }},// 计算分割后的文件 hash 值calcuateFileHash(splitedFiles:Array<Blob>,chunkSize:number):Promise<string>{letspark =newsparkMD5.ArrayBuffer();letchunks:Blob[]=[];splitedFiles.forEach((chunk,idx)=>{if(idx ===0||idx ===splitedFiles.length -1){chunks.push(chunk);}else{// 中间剩余切片分别在前面、
- 创建并发上传任务:
- 使用 map 方法遍历 formDataArr,为每个分片创建一个上传任务。
- 选择文件片段:
- 遍历 splitedFiles 数组,该数组包含了文件的所有分片。分片哈希值 chunkHash 和分片总数 chunkTotal。file.slice 方法接受两个参数:起始位置和结束位置,分别对应当前分片的开始和结束字节。
- 错误处理:
- 如果在处理过程中发生错误(例如文件系统操作失败),则捕获错误,删除临时文件夹 UPLOAD_MULTER_TEMP_DIR 的内容,并返回状态码 500,表示服务器内部错误。
- 返回部分成功信息:
- 如果分片上传成功但未达到分片总数,则返回状态码 200 和部分成功信息,返回已上传的分片列表和分片总数。
- 检查文件是否已存在:
- 使用 isFileOrDirInExist 函数检查服务器上是否已存在完整的文件(文件名由 fileHash 和 fileName 组成)。
- 读取文件片段:
- 创建一个 FileReader 实例,用于读取文件片段。
- 如果上传成功,更新上传进度。
- 在循环中,使用 file.slice 方法从文件中切出每个分片。
- originFile:原始文件对象。分片总数、
- name:文件名。这样可以减少计算量,同时保持一定的哈希准确性。
- 在每个上传任务中,使用 postUploadFile 函数发送上传请求,并传递 FormData 和取消令牌。分片总数 chunkTotal 和文件名 fileName。
- 生成表单数据:
- formDataArr:调用 genFormDataByChunkInfo 方法,根据分片信息生成对应的 FormData 对象数组。
- chunkTotalNum:获取分片总数。
生成文件MD5
letfileMd5 =awaitthis.calcuateFileHash(fileSplitedObj.splitedFile,this.chunkSize);// 计算分割后的文件 hash 值calcuateFileHash(splitedFiles:Array<Blob>,chunkSize:number):Promise<string>{letspark =newsparkMD5.ArrayBuffer();letchunks:Blob[]=[];splitedFiles.forEach((chunk,idx)=>{if(idx ===0||idx ===splitedFiles.length -1){chunks.push(chunk);}else{// 中间剩余切片分别在前面、如果缺少必要的参数,则返回状态码 200 和错误信息,提示缺少必要的参数。
- 从请求体 req.body 中获取分片索引 index、
请求体中包含文件的哈希值 fileHash、
- 创建切片目录:
- 使用 fse.access 检查临时切片目录 tempChunkDir 是否存在,如果不存在,则使用 fse.mkdir 创建该目录。
- 等待所有上传任务完成:
使用 Promise.all 等待所有上传任务完成,返回一个包含所有上传结果的数组。后面和中间取2个字节参与计算chunks.push(chunk.slice(0,2));// 前面的2字节chunks.push(chunk.slice(chunkSize /2,(chunkSize /2)+2));// 中间的2字节chunks.push(chunk.slice(chunkSize -2,chunkSize));// 后面的2字节}});returnnewPromise((resolve,reject)=>{letreader =newFileReader();//异步 APIreader.readAsArrayBuffer(newBlob(chunks));reader.onload=(e:Event)=>{spark.append((e.target asany).result asArrayBuffer);resolve(spark.end());};reader.onerror=()=>{reject('');};})},// 生成 formDatagenFormDataByChunkInfo(chunkList:Array<{fileName:string,fileHash:string,index:number,chunk:Blob,chunkHash:string,size:number,chunkTotal:number}>){returnchunkList.map(({fileName,fileHash,index,chunk,chunkHash,chunkTotal,size })=>{letformData =newFormData();formData.append('chunk',chunk);formData.append('chunkHash',chunkHash);formData.append('size',String(size));formData.append('chunkTotal',String(chunkTotal));formData.append('fileName',fileName);formData.append('fileHash',fileHash);formData.append('index',String(index));returnformData;});},// 取消请求createReqControl(){letcancelToken =axios.CancelToken;letcancelReq:CancelTokenSource[]=[];return{addCancelReq(req:CancelTokenSource){cancelReq.push(req);},cancelAllReq(msg ='已取消请求'){cancelReq.forEach((req)=>{req.cancel(msg);// 全部取消后续请求})},createSource(){returncancelToken.source();},print(){console.log(cancelReq);}}},// 上传文件前的检查asyncuploadFileCheck(splitedFileObj:{originFile:File,name:string,splitedFile:Array<Blob>},fileMd5:string):Promise<{isError:booleanisFileExist:boolean,uploadedChunks:[]}>{try{let{data }=awaitpostUploadFileCheck({fileHash:fileMd5,chunkTotal:splitedFileObj.splitedFile.length,fileName:splitedFileObj.name });if(data.code ===200&&!(data.result?.isFileExist)){return{isError:false,isFileExist:data.result?.isFileExist,uploadedChunks:data.result.uploadedChunks };}return{isError:true,isFileExist:false,uploadedChunks:[]};}catch(error){return{isError:true,isFileExist:false,uploadedChunks:[]}}},// 并发请求asyncuploadFilesConcurrently(splitedFileObj:{originFile:File,name:string,splitedFile:Array<Blob>},fileMd5:string,concurrentNum =3,uploadedChunks:Array<number>){letcancelControlReq =this.createReqControl();constLIMIT_FUN=pLimit(concurrentNum);// 初始化并发限制letfileName =splitedFileObj.name;// 文件名letchunkTotalNum =splitedFileObj.splitedFile.length;letchunkList =splitedFileObj.splitedFile .map((chunk,idx)=>{if(uploadedChunks.includes(idx))returnnull;return{fileName,fileHash:fileMd5,index:idx,chunk,chunkTotal:chunkTotalNum,chunkHash:`${fileMd5 }-${idx }`,size:chunk.size }}).filter((fileInfo)=>fileInfo !=null);// 过滤掉已经上传的chunkletformDataArr =this.genFormDataByChunkInfo(chunkList);letallPromises =formDataArr.map((formData)=>{letsource =cancelControlReq.createSource();// 生成sourcecancelControlReq.addCancelReq(source);//添加 sourcereturnLIMIT_FUN(()=>newPromise(async(resolve,reject)=>{try{letresult =awaitpostUploadFile(formData,source.token);if(result.data.code ===100){cancelControlReq.cancelAllReq();// 取消后续全部请求}if(result.data.code ===201||result.data.code ===200){letdata =result.data.result;this.setPropress(Number(data.uploadedChunks.length),Number(data.chunkTotal));}resolve(result);}catch(error){this.setPropress(0,0);// 关闭进度条// 报错后取消后续请求cancelControlReq.cancelAllReq();// 取消后续全部请求reject(error);}}));})returnawaitPromise.all(allPromises);},// 设置进度条setPropress(uploadedChunks:number,chunkTotal:number){this.processVal =(uploadedChunks /chunkTotal)*100;},// 文件上传asyncuploadFile(){// 获取文件输入元素中的文件列表letfiles =(this.$refs.fileInput asHTMLInputElement).files ||[];if(files.length <=0)return;// 将选择的文件按照指定的分片大小进行分片处理 letfileSplitedObj =this.splitFileByChunkSize(files[0],this.chunkSize);// 计算整个文件的哈希值,用于后续的文件校验和秒传功能letfileMd5 =awaitthis.calcuateFileHash(fileSplitedObj.splitedFile,this.chunkSize);// 检查服务器上是否已存在该文件的分片以及整个文件letuploadedChunksObj =awaitthis.uploadFileCheck(fileSplitedObj,fileMd5);// 如果检查过程中发生错误,或者文件已存在,则直接返回 if(!(!uploadedChunksObj.isError &&!uploadedChunksObj.isFileExist))return;// 并发上传文件分片,最多同时上传3个分片letuploadFileResultArr =awaitthis.uploadFilesConcurrently(fileSplitedObj,fileMd5,3,uploadedChunksObj.uploadedChunks );// 上传成功后,重置进度条if(uploadFileResultArr &&Array.isArray(uploadFileResultArr)){this.setPropress(0,0);}}}});uploadFile函数逻辑分析
检查是否选择了要上传的文件
letfiles =(this.$refs.fileInput asHTMLInputElement).files ||[];if(files.length <=0)return;// 没有选择文件,后续就不走
文件分片
letfileSplitedObj =this.splitFileByChunkSize(files[0],this.chunkSize);// 分割文件splitFileByChunkSize(file:File,chunkSize:number){letsplitedFileArr =[];letfileSize =file.size;// 获取文件大小lettotalChunkNumber =Math.ceil(fileSize /chunkSize);// 向上取整获取 chunk 数量for(leti =0;i <totalChunkNumber;i++){// File类型继承BlobsplitedFileArr.push(file.slice(i *chunkSize,(i +1)*chunkSize));}return{originFile:file,name:file.name,splitedFile:splitedFileArr }},
splitFileByChunkSize 函数功能分析
- 初始化变量:
- splitedFileArr:用于存储分割后的文件分片数组。每个分片可以使用 Blob.slice 方法从文件对象中切出
- 文件哈希
计算哈希值:- 使用 Web Workers 来计算每个分片的哈希值,以避免阻塞主线程。
nodeJs 逻辑
index入口文件
constEXPRESS=require('express');constPATH=require('path');constHISTORY=require('connect-history-api-fallback');constCOMPRESSION=require('compression');constREQUEST=require('./routes/request');constENV=require('./config/env');constAPP=EXPRESS();constPORT=3000;APP.use(COMPRESSION());// 开启gzip压缩// 设置静态资源缓存constSERVE=(path,maxAge)=>EXPRESS.static(path,{maxAge });APP.use(EXPRESS.json());APP.all('*',(req,res,next)=>{res.header("Access-Control-Allow-Origin","*");res.header("Access-Control-Allow-Headers","Content-Type");res.header("Access-Control-Allow-Methods","*");next()});APP.use(REQUEST);APP.use(HISTORY());// 重置单页面路由APP.use('/dist',SERVE(PATH.resolve(__dirname,'../dist'),ENV.maxAge));//根据环境变量使用不同环境配置APP.use(require(ENV.router));APP.listen(PORT,()=>{console.log(`APP listening at http://localhost:${PORT}\n`);});
request处理请求
constexpress =require('express');constrequestRouter =express.Router();const{resolve,join }=require('path');constmulter =require('multer');constUPLOAD_DIR=resolve(__dirname,'../upload');constUPLOAD_FILE_DIR=join(UPLOAD_DIR,'files');constUPLOAD_MULTER_TEMP_DIR=join(UPLOAD_DIR,'multerTemp');constupload =multer({dest:UPLOAD_MULTER_TEMP_DIR});constfse =require('fs/promises');constfs =require('fs');require('events').EventEmitter.defaultMaxListeners =20;// 将默认限制增加到// 合并chunksfunctionmergeChunks(fileName,tempChunkDir,destDir,fileHash,chunks,cb){letwriteStream =fs.createWriteStream(`${destDir }/${fileHash }-${fileName }`);writeStream.on('finish',async()=>{writeStream.close();// 关闭try{awaitfse.rm(tempChunkDir,{recursive:true,force:true});}catch(error){console.error(tempChunkDir,error);}})letreadStreamFun=function(chunks,cb){try{letval =chunks.shift();letpath =join(tempChunkDir,`${fileHash }-${val }`);letreadStream =fs.createReadStream(path);readStream.pipe(writeStream,{end:false});readStream.once('end',()=>{console.log('path',path);if(fs.existsSync(path)){fs.unlinkSync(path);}if(chunks.length >0){readStreamFun(chunks,cb);}else{cb();}});}catch(error){console.error(error);}}readStreamFun(chunks,()=>{cb();writeStream.end();});}// 判断当前文件是否已经存在functionisFileOrDirInExist(filePath){returnfs.existsSync(filePath);};// 删除文件夹内的内容胆保留文件夹functionrmDirContents(dirPath){fs.readdirSync(dirPath).forEach(file=>{letcurPath =join(dirPath,file);if(fs.lstatSync(curPath).isDirectory()){rmDirContents(curPath);}else{fs.unlinkSync(curPath);}});}// 获取已上传chunks序号asyncfunctiongetUploadedChunksIdx(tempChunkDir,fileHash){if(!isFileOrDirInExist(tempChunkDir))return[];// 不存在直接返回[]letuploadedChunks =awaitfse.readdir(tempChunkDir);letuploadedChunkArr =uploadedChunks.filter(file=>file.startsWith(fileHash +'-')).map(file=>parseInt(file.split('-')[1],10));return[...(newSet(uploadedChunkArr.sort((a,b)=>a -b)))];}requestRouter.post('/api/upload/check',asyncfunction(req,res){try{letfileHash =req.body?.fileHash;letchunkTotal =req.body?.chunkTotal;letfileName =req.body?.fileName;lettempChunkDir =join(UPLOAD_DIR,'temp',fileHash);// 存储切片的临时文件夹if(!fileHash ||chunkTotal ==null){returnres.status(200).json({code:400,massage:'缺少必要的参数',result:null});}letisFileExist =fs.existsSync(join(UPLOAD_FILE_DIR,`${fileHash }-${fileName }`));// 如果文件存在,则清除temp中临时文件和文件夹if(isFileExist){// 当前文件夹存在if(fs.existsSync(tempChunkDir)){fs.rmSync(tempChunkDir,{recursive:true,force:true});}returnres.status(200).json({code:200,massage:'成功',result:{uploadedChunks:[],isFileExist }})}letduplicateUploadedChunks =awaitgetUploadedChunksIdx(tempChunkDir,fileHash);returnres.status(200).json({code:200,massage:'成功',result:{uploadedChunks:duplicateUploadedChunks,isFileExist }});}catch(error){console.error(error);rmDirContents(UPLOAD_MULTER_TEMP_DIR);// 删除临时文件returnres.status(500).end();}});requestRouter.post('/api/upload',upload.single('chunk'),asyncfunction(req,res){try{letchunk =req.file;// 获取 chunkletindex =req.body?.index;letfileName =req.body?.fileName;letfileHash =req.body?.fileHash;// 文件 hashletchunkHash =req.body?.chunkHash;letchunkTotal =req.body?.chunkTotal;// chunk 总数lettempChunkDir =join(UPLOAD_DIR,'temp',fileHash);// 存储切片的临时文件夹if(isFileOrDirInExist(join(UPLOAD_FILE_DIR,`${fileHash }-${fileName }`))){rmDirContents(UPLOAD_MULTER_TEMP_DIR);// 删除临时文件returnres.status(200).json({code:100,massage:'该文件已存在',result:fileHash }).end();}// 切片目录不存在,则创建try{awaitfse.access(tempChunkDir,fse.constants.F_OK)}catch(error){awaitfse.mkdir(tempChunkDir,{recursive:true});}if(!fileName ||!fileHash){res.status(200).json({code:400,massage:'缺少必要的参数',result:null});}awaitfse.rename(chunk.path,join(tempChunkDir,chunkHash));letduplicateUploadedChunks =awaitgetUploadedChunksIdx(tempChunkDir,fileHash);// 获取已上传的chunks// 当全部chunks上传完毕后,进行文件合并if(duplicateUploadedChunks.length ===Number(chunkTotal)){mergeChunks(fileName,tempChunkDir,UPLOAD_FILE_DIR,fileHash,duplicateUploadedChunks,()=>{rmDirContents(UPLOAD_MULTER_TEMP_DIR);// 删除临时文件res.status(200).json({code:200,massage:'成功',result:{uploadedChunks:newArray(Number(chunkTotal)).fill().map((_,index)=>index),chunkTotal:Number(chunkTotal)}})});}else{res.status(200).json({code:201,massage:'部分成功',result:{uploadedChunks:duplicateUploadedChunks,chunkTotal:Number(chunkTotal)}})}}catch(error){console.error(error);rmDirContents(UPLOAD_MULTER_TEMP_DIR);// 删除临时文件returnres.status(500).end();}});module.exports =requestRouter;
/api/upload/check接口分析
- 获取请求参数:
- 从请求体 req.body 中获取文件的哈希值 fileHash、
效果
WeChat_20250104153438
- 错误处理:
在 FileReader 的 onerror 事件中,如果读取文件片段发生错误,则通过 reject 回调函数返回一个空字符串,表示哈希计算失败。 - 对于中间的分片,只选择每个分片的前 2 个字节、
检查是否已存在该文件的分片以及整个文件
letuploadedChunksObj =awaitthis.uploadFileCheck(fileSplitedObj,fileMd5);// 如果检查过程中发生错误,或者文件已存在,则直接返回 if(!(!uploadedChunksObj.isError &&!uploadedChunksObj.isFileExist))return;// 上传文件前的检查asyncuploadFileCheck(splitedFileObj:{originFile:File,name:string,splitedFile:Array<Blob>},fileMd5:string):Promise<{isError:booleanisFileExist:boolean,uploadedChunks:[]}>{try{let{data }=awaitpostUploadFileCheck({fileHash:fileMd5,chunkTotal:splitedFileObj.splitedFile.length,fileName:splitedFileObj.name });if(data.code ===200&&!(data.result?.isFileExist)){return{isError:false,isFileExist:data.result?.isFileExist,uploadedChunks:data.result.uploadedChunks };}return{isError:true,isFileExist:false,uploadedChunks:[]};}catch(error){return{isError:true,isFileExist:false,uploadedChunks:[]}}},
uploadFileCheck 函数功能分析
- 参数接收:
- splitedFileObj:包含原始文件信息和分割后的文件分片数组的对象。
- LIMIT_FUN:使用 pLimit 函数限制并发上传的数量。 * 4.断点续传:在上传过程中,如果发生中断,下次再上传可以从中断点继续上传,而不是重新上传整个文件。
- fileMd5:文件的哈希值。后面和中间取2个字节参与计算chunks.push(chunk.slice(0,2));// 前面的2字节chunks.push(chunk.slice(chunkSize /2,(chunkSize /2)+2));// 中间的2字节chunks.push(chunk.slice(chunkSize -2,chunkSize));// 后面的2字节}});returnnewPromise((resolve,reject)=>{letreader =newFileReader();//异步 APIreader.readAsArrayBuffer(newBlob(chunks));reader.onload=(e:Event)=>{spark.append((e.target asany).result asArrayBuffer);resolve(spark.end());};reader.onerror=()=>{reject('');};})},
calcuateFileHash 函数功能分析
- 初始化变量:
- spark:创建一个 sparkMD5.ArrayBuffer 实例,用于计算文件的 MD5 哈希值。
- 调用 spark.end() 方法计算最终的 MD5 哈希值,并通过 resolve 回调函数返回该哈希值。
- 对于第一个和最后一个分片,直接将它们添加到 chunks 数组中。分片总数 chunkTotal 和文件名 fileName。 * 6.错误处理:在上传过程中,要及时处理可能出现的错误,如网络错误、
- 获取已上传的分片索引:
- 调用 getUploadedChunksIdx 函数获取已上传的分片索引列表。
- 初始化变量: