我不会在后台详细说明公司的业务问题。一般要求是需要一个resume函数。公司的文件存储是amazons3,是通用的文件存储,类似于阿里云的oss。可以对文件对象进行一些操作的对象。Amazons3是一种可以创建存储桶来存储文件的服务。对于一个bucket,有三级权限:1.publicread,publicwrite;2.公有读,私有写;3.私有读,私有写为什么这个功能需要前端来完成?因为后端只提供数据接口,不提供web服务。Web服务由前端自行开发和解决。如果你不了解amazons3或者阿里云的oss,可能很难看懂这篇文章。简单文件上传到amazon的s3,文件直接上传有几种选择,分为:前端直接上传和sdk上传1.前端直接上传,分为公开写和私有写。公写:可以直接拼接url:http://\
/\/\,可以直接向这个url发起前端文件的put请求,其中address是服务提供的域名,bucketname是我们的桶名,objectname是对象名(也就是文件名)私写:必须使用miniosdk。使用sdk提供的presignedPutObject方法生成上传地址。前端拿到链接后,也可以直接在前端发起put请求。注意sdk只能跑在服务器上,也就是必须要有自己的服务。2.使用SDK上传。SDK只能在服务器上运行。流程基本就是前端上传文件到服务器,服务器使用SDK上传文件到S3文件服务。这两种方案,除了公写外,其他都需要有自建服务器,从安全的角度来说,设置公写是不合理的。你必须有自己的服务。和大多数人一样,在做这个断点续传的功能时,我也在网上搜索了别人的方法,找到合适的方法后,结合我们的项目,设计了这样一个解决方案:用户选择一个后文件,前端使用file.slice对文件进行切片计算文件hash,也就是文件的md5。只要文件内容保持不变,文件哈希就不会改变。计算哈希是一个同步任务。如果文件太大,浏览器会卡住。我使用spark-md5+浏览器的requestIdleCallbackAPI来解决问题。当然可以根据自己项目的情况使用webworker。也可以使用hash查询是否有对应的文件。这个可以有自己的数据库,存储hash对应的文件链接。如果使用hash作为文件名,也可以调用minio的sdk的statObject方法知道文件是否已经上传。如果已经有文件信息,可以直接获取链接,无需上传文件。这就是即时传输的功能。如果没有上传,读取本地服务器。这个hash命名的文件夹下已经上传了哪些分片,将上传的分片返回给前端,前端可以选择性的上传未上传的分片,文件的哈希值和分片数量等信息将作为参数传递给服务器。服务器拿到文件后,会以hash命名文件夹,以分片个数命名分片。片段存储在服务器上。所有分片上传完毕后,在服务器上合并文件,然后调用minio的putObject方法将文件上传到S3文件服务器。上传完成后,服务器本地文件被删除,整个断点续传。功能是完整的,但是这个方案有一个不完善的地方,就是需要在服务器端对分片文件进行存储合并,合并后上传到文件服务。据我了解,标准的S3本身就有断点续传和上传合并文件的功能,能不能在直接上传分片的时候把分片上传到S3文件服务器,上传完后直接在S3文件服务器上合并文件片段上传了吗?答案在后面,但当时我没有找到可用的解决方案。唯一找到的解决方法是百度智能云提供的api:https://cloud.baidu.com/doc/B...但是minio的sdk并没有提供uploadPart方法,但是这个方法不行,所以我们只能先放下问题。上面的方案有个致命的问题没有考虑,就是有多台机器在线,这些部分会上传到不同的机器上,文件会合并。使用时无法合并,所以不能使用上面设计的方案。这个问题是在解决合并文件时发现找不到碎片的问题才考虑的,所以需要重新考虑新的。解决方案,重点是如何将上传的分片直接上传到S3文件服务器,并在S3文件服务器上进行合并。为了解决这个方案,看了minio的源码,看了putObject的源码后,了解到putObject的核心流程如下:使用block-stream2将文件分块使用objectName调用findUploadId查询uploadId,如果没有uploadId,会调用initiateNewMultipartUpload初始化一个uploadId,通过自己的一些测试可以得到一些信息:2.1每次调用initiateNewMultipartUpload返回的uploadId都不一样2.2findUploadId会返回最新的uploadId2.3通过查找其他信息,我们知道uploadId有7天调用listParts获取上传的分片组合参数,调用makeRequest上传分片,调用completeMultipartUpload完成分片上传。这个方法会合并所有的部分,并返回合并后的文件etag新的解决方案通过查看putObject方法的源代码,我们可以对我们现有的方案进行一些修改。用户选择文件后,对文件进行分片计算文件哈希,与前面一致。文件哈希用作新文件名,或者添加固定前缀。它必须按照固定的规则命名。同一个文件最好不要名字不一致,因为minio的服务器名是唯一key。通过文件名来获取文件是否已经存在,主要是调用minio的statObject方法。如果不存在,则获取带文件名的uploadId,然后使用uploadId获取上传的分片(这个和前面的不同,因为分片文件在服务器本地不存在,所以必须存储分片信息在数据库中,其实也可以调用sdk的listParts方法获取上传的分片,但是调用listParts返回的信息没有携带预期的partNumber参数,可能是公司自建S3服务的原因,所以分片只能存入仓库)前端拿到上传信息后,和之前一样。如果文件已经存在,则不会上传,否则计算需要先上传分片再上传自研的uploadPart方法。服务器收到分片后,获取分片文件的ArrayBuffer,获取uploadId,组装参数,调用sdk的makeRequest方法将分片上传到S3文件服务器服务器上传完成后删除文件分片,并放入将上传的片段信息存入数据库。前端接收到所有上传的分片后,调用合并文件接口,服务端合并文件,调用sdk的completeMultipartUpload方法。S3文件服务器服务器上的所有碎片合并到这个新方案中,下面贴出前端部分代码:文件碎片:functioncreateChunks(file,size=SINGLECHUNKSIZE){令cur=0,index=1;常量块=[];while(cur{returnnewPromise((resolve)=>{constspark=newSparkMD5.ArrayBuffer();letcount=0;constworkLoop=async()=>{if(count{constadd=(deadline)=>{if(deadline.timeRemaining()>1){spark.append(e.target.result);c伯爵++;constprogress=parseInt((count/chunks.length)*100)/100;if(count0){返回真}constclient=newClient({endPoint:this.config.S3文件服务器v3.endPoint,accessKey:this.config.S3文件服务器v3.accessKey,secretKey:this.config.S3文件服务器v3.secretKey,})constchunk=awaitfse.readFile(file.filepath)constquery=querystring.stringify({partNumber:part,uploadId,})constoptions={method:'PUT',query,headers:{'Content-Length':chunk.length,'Content-Type':mime.lookup(filename),},bucketName:this.config.S3文件服务器v3.bucketName,objectName:filename,}constetag=awaitnewPromise((resolve,reject)=>{client.makeRequest(options,chunk,[200],'',true,function(err,response,){if(err)returnreject(err)//为了将各个部分聚合在一起,我们需要收集etag。让etag=响应。标题。etag如果(etag){etag=etag。replace(/^"/,'').replace(/"$/,'')}fse.unlink(file.filepath)resolve(etag)})})constinsertResult=awaitthis.ctx.model.etagCenter.add({filename,etag,part,uploadId,})returninsertResult.insertedCount>0}方案和代码还有很多需要改进的地方,我们还在思考后续的优化。现在分片文件需要先上传到我们自己的服务,再上传到S3文件服务器,需要一定的时间和效率。未来希望可以将分片文件直接上传到前端的S3文件服务器。当然会在保证安全的前提下,只是目前没有时间考虑这个。如果有效,后续优化会往这个方向走