我们要上传一个1G+的大文件,前端采用分片上传,但是由于某种原因比如要下班了,断电了或是网线被拔掉了,上传被迫中断。那么别急,我们有断点续传功能,你可以将大文件带回家慢慢从中断处继续上传,而不需要重新上传整个文件。
断点续传原理及流程
文章讲解了《02.文件分片上传之前端文件分片》,我们将文件分片后,一片一片依次上传给后端。后端将这些分片文件存储在一个临时目录下,等待所有分片上传完毕再合并一个完整文件。
文件上传时,会携带文件唯一标识,也就是MD5值以及分片信息。后端按照md5保存分片,即名称为同一md5的分片属于同一个文件。(理论上也有md5值冲突的,但是几率很少)。
当某种原因导致上传中断后,后端会保存已经上传好的分片。
当前端发送继续上传请求时,后端会查找该文件是否有已经上传好的分片,如果有,那么将这些分片id返回给前端,告诉前端这些分片不必再上传了,从断点处继续上传剩余的分片。
所有分片上传完毕,合并文件。
前端发送续传请求
我们在上一篇文章《05.文件上传之秒传文件》讲到,在vue-simple-uploader的options选项中添加函数checkChunkUploadedByResponse()
,该函数响应后台返回message信息,同时检测分片信息是否上传完整。上传分片前,前端会携带文件md5等信息先向后端发送一个get请求,这个checkChunkUploadedByResponse()
就是用来响应这个get请求的。
// 服务器分片校验函数
checkChunkUploadedByResponse: (chunk, message) => {
let obj = JSON.parse(message);
if (obj.isExist) {
this.statusTextMap.success = '秒传文件';
return true;
}
return (obj.uploaded || []).indexOf(chunk.offset + 1) >= 0
},
很显然,如果返回obj.isExist
则表示文件已经存在,应该是秒传文件,该文件的所有后续上传操作就没了。
如果返回的是文件分片信息,类似:{"uploaded":[1,2,3,4,5,6,7,8,9,10,...]}
,表示已经上传过了该文件的这些分片。
那么接下来就是续传,返回接下来应该从第几个分片续传,(obj.uploaded || []).indexOf(chunk.offset + 1) >= 0
意思是从断点分片处继续上传下一个分片。当然了,如果没有上传过分片,那就是从第一个分片起开始上传。
后端分析断点,接受续传
后端在接收前端发来的get请求,先检测md5,确定是否应该秒传文件,然后检测已经上传了哪些分片,将这些分片id合并成数组,返回给前端。
//检测断点和md5
public function checkFile()
{
$identifier = $this->fileInfo['identifier'];
$filePath = self::$tmpDir. DIRECTORY_SEPARATOR . $identifier; //临时分片文件路径
$totalChunks = $this->fileInfo['totalChunks'];
//检测文件md5是否已经存在
$rs = $this->checkMd5($identifier, $this->fileInfo['totalSize']);
if ($rs['isExist'] === true) {
return $rs;
}
//检查分片是否存在
$chunkExists = [];
for ($index = 1; $index <= $totalChunks; $index++ ) {
if (file_exists("{$filePath}_{$index}")) {
array_push($chunkExists, $index);
}
}
if (count($chunkExists) == $totalChunks) { //全部分片存在,则直接合成
$this->merge();
} else {
$res['uploaded'] = $chunkExists;
return $res;
}
}
在分片临时目录保存的是文件的分片,分片的命名形式:唯一标识_第几个分片。如:d816ed041157c4af2489148a1ffd60d4_29,表示md5值为d816ed041157c4af2489148a1ffd60d4第29个分片。这样大家就明白了,根据分片总数,遍历该文件的所有分片,将分片id即第几个分片追加到数组中$chunkExists
,如果所有分片已经和分片总数相等时应该合并文件,否则返回字段:$res['uploaded'] = $chunkExists
,最终给到前端的是json格式数据。
试验
最后我们来测试断点续传。
首先打开你的Chrome浏览器利器,运行上传页面,选择一个大点的文件,这里我选择了一个1.9GB的系统镜像文件,开始上传。
现在,我们关闭这个浏览器,断开网络。你可以断电,重启电脑等方式模拟上传中断,然后把文件带回家。
然后使用另外一个浏览器,我临时用以下360浏览器,模拟跨浏览器场景。
注意重新选择同一个文件上传。在上传前检测md5值是必不可少的,大文件的md5计算速度也会相对较长一点。
计算完md5后,前端先是向后端发送一个get请求,看看是否是秒传,应急看看是否已经传过一部分了,应该断点续传。
很显然,后端返回了已经上传的分片,那么就该从断点处续传。
我们到服务端打开分片临时目录可以看到:
注意图中红框部分,第一次上传后,分片id编号是135,时间是17:02,下一个分片id是136,时间是17:05。也就是说第二次上传的时候是从136开始的,之前的分片并没有被删除也没有被覆盖,最后等所有分片上传完毕就合并成一个完整的文件。
于是就实现了跨浏览器跨终端续传文件。
关于文件上传的总结
文件上传的关键是分片和计算md5,根据文件分片信息和md5值,可以实现分片上传、秒传文件、断点续传等功能。
不管是秒传文件还是续传文件,第一步计算md5是不能跨越的。
服务端文件的保存方式,一般设置一个临时目录保存分片信息,合并后移除分片,并将最终文件保存在另一个目录下。根据项目需求,文件保存的目录可以是在web目录下,也可以是在web访问不到的目录。