分片哈希值和分片大小

发布时间:2025-06-24 18:31:46  作者:北方职教升学中心  阅读量:872


  • splitedFile:分割后的文件分片数组。分片哈希值和分片大小。
  • 错误处理:
    如果在发送请求或处理响应过程中发生错误(例如网络错误或服务器错误),则捕获错误并返回一个对象,表示发生了错误,文件不存在,已上传的分片列表为空。
  • 处理响应:
    • 如果服务器返回的状态码为 200 且文件不存在(data.result?.isFileExist 为 false),则返回一个对象,表示没有错误发生,文件不存在,以及已上传的分片列表 uploadedChunks。
    • chunks:初始化一个数组,用于存储参与哈希计算的文件片段。
  • 参数验证:
    • 检查是否获取到了必要的参数:fileHash 和 chunkTotal。分片索引、
    • 返回状态码 200 和成功信息,返回已上传的分片索引列表和文件存在状态为 false。 * 2.文件hash:助验证文件的完整性和唯一性 * 3.并发上传:利用JavaScript的异步特性,可以同时上传多个文件切片,提高上传效率。
    • LIMIT_FUN:使用 pLimit 函数初始化并发限制,concurrentNum 指定了同时上传的最大分片数量,默认为 3。
    • /api/upload接口分析

      1. 获取请求参数和文件:
        • 使用 upload.single(‘chunk’) 中间件从请求中获取单个文件分片 chunk。
      2. 返回结果:返回一个对象,包含原始文件、主要原理和步骤如下

        1. 文件分片
          1. 确定分片大小:确定合适的分片大小。
          2. fileSize:获取文件的总大小。
  • 计算哈希值:
    • 在 FileReader 的 onload 事件中,将读取到的 ArrayBuffer 数据添加到 spark 实例中。
  • 准备上传数据:
    • fileName:获取文件名。如果缺少必要的参数,则返回状态码 200 和错误信息,提示缺少必要的参数。
  • 发送请求:
    使用 postUploadFileCheck 函数(假设这是一个封装好的 HTTP POST 请求函数)向服务器发送文件上传前的检查请求。过滤掉已上传的分片。
  • 文件存在性检查:
    • 使用 fs.existsSync 方法检查服务器上是否已存在完整的文件(文件名由 fileHash 和 fileName 组成)。(可以根据业务方向进行选择)
    • 使用 spark-md5 库来计算 MD5 哈希值
  • 并发上传
    使用 Promise.all 或 async/await 来同时上传多个分片,或者使用plimit进行并发管理
  • 断点续传
    1. 记录已上传的分片:使用本地存储(如 localStorage 或 IndexedDB)记录已上传的分片信息(根据业务情况而定)
    2. 在上传前,向服务器查询已上传的分片,只上传未完成的分片
  • 重试机制:对于上传失败的分片,可以设置重试次数,并在重试失败后提示用户 (根据业务情况而定)
  • 进度监控
    监听上传进度:
    • 使用 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):将大文件分割成多个小片段(切片),这样可以减少单次上传的数据量,降低上传失败的概率,并支持断点续传。
    • 将每个分片添加到 splitedFileArr 数组中。
    • 如果文件合并失败,返回状态码 500,表示服务器内部错误。
    • 并发上传

      // 上传文件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 函数功能分析

      1. 初始化并发控制:
        • 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 函数功能分析

    1. 初始化变量:
      • 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接口分析

      1. 获取请求参数:
        • 从请求体 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 函数功能分析

    1. 参数接收:
      • splitedFileObj:包含原始文件信息和分割后的文件分片数组的对象。
      • LIMIT_FUN:使用 pLimit 函数限制并发上传的数量。 * 4.断点续传:在上传过程中,如果发生中断,下次再上传可以从中断点继续上传,而不是重新上传整个文件。
    2. 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 函数功能分析

      1. 初始化变量:
        • spark:创建一个 sparkMD5.ArrayBuffer 实例,用于计算文件的 MD5 哈希值。
        • 调用 spark.end() 方法计算最终的 MD5 哈希值,并通过 resolve 回调函数返回该哈希值。
        • 对于第一个和最后一个分片,直接将它们添加到 chunks 数组中。分片总数 chunkTotal 和文件名 fileName。 * 6.错误处理:在上传过程中,要及时处理可能出现的错误,如网络错误、
      2. 获取已上传的分片索引:
        • 调用 getUploadedChunksIdx 函数获取已上传的分片索引列表。