为了阅读和开发方便,我将文件上传系列相关文章章节列出来:
01.使用vue-simple-uploader上传文件和文件夹
02.文件分片上传之前端文件分片
03.文件分片上传之后端PHP合成文件
04.超大文件上传之计算文件MD5值
05.文件上传之秒传文件
06.文件上传之断点续传和跨端续传
在上一节文章中,我们知道,当所有的分片都上传完成时,会调用onFileSuccess()
方法。
onFileSuccess(rootFile, file, response, chunk) { let resp = JSON.parse(response); if (resp.code === 0 && resp.merge === false) { console.log(\'上传成功,不需要合并\'); } else { axios.post(\'http://localhost:9999/up.php?action=merge\', { filename: file.name, identifier: file.uniqueIdentifier, totalSize: file.size, totalChunks: chunk.offset + 1 }).then(function(res){ if (res.code === 0) { console.log(\'上传成功\') } else { console.log(res.message); } }) .catch(function(error){ console.log(error); }); } },
从后台返回的response
包含了是否需要合并的指令merge
,如果resp.merge === true
,那就发送合并请求,告诉后端可以合成分片了。如果上传的文件只有一片,就不需要合并。
我们计划用PHP写一个处理上传的类,负责检测文件、接收上传分片、合并分片等。当中还要用到数据库存储文件信息,这些我们在后面章节完成,先看结构:
\'\', //文件的唯一标识 \'chunkNumber\' => 1, //当前是第几个分片 \'totalChunks\' => 1, //总分片数 \'filename\' => \'\', //文件名称 \'totalSize\' => 0 //文件总大小 ]; //检测断点和md5 public function checkFile() { // } //上传分片 public function upload() { // } //合并文件 public function merge() { } //计算时间 private function getmicrotime() { list($usec, $sec) = explode(" ",microtime()); return ((float)$usec + (float)$sec); } //返回提示消息 private function message($code, $msg) { $res = [ \'code\' => $code, \'message\' => $msg ]; return $res; } }
我们先定义上传目录,整个目录可以是在你的web目录,也可以是web访问不到的目录,一个临时目录files_tmp/用来保存临时分片文件,一个是真正保存文件的目录files/,注意我们是在Wind平台运行,如果是为Linux下,路径应该写成像这样:/opt/data/files。此外这两个目录要有写权限。
首先我们接收前端上传上来的分片文件,当然在正式接收上传分片前,应该检测文件是否已经上传过了,检测文件合法性等等,这些我们在后续文章中会讲到。我们先来看PHP如何接收分片文件。
public function upload() { if (!empty($_FILES)) { $in = @fopen($_FILES["file"]["tmp_name"], "rb"); if (!$in = @fopen($_FILES["file"]["tmp_name"], "rb")) { return $this->message(1002, \'打开临时文件失败\'); } } else { if (!$in = @fopen("php://input", "rb")) { return $this->message(1003, \'打开输入流失败\'); } } if ($this->fileInfo[\'totalChunks\'] === 1) { //如果只有1片,则不需要合并,直接将临时文件转存到保存目录下 $filename = $this->fileInfo[\'filename\']; $saveDir = self::$saveDir . DIRECTORY_SEPARATOR . date(\'Y-m-d\'); if (!is_dir($saveDir)) { @mkdir($saveDir); } $uploadPath = $saveDir . DIRECTORY_SEPARATOR .$filename; $res[\'merge\'] = false; } else { //需要合并 $filePath = self::$tmpDir. DIRECTORY_SEPARATOR . $this->fileInfo[\'identifier\']; //临时分片文件路径 $uploadPath = $filePath . \'_\' . $this->fileInfo[\'chunkNumber\']; //临时分片文件名 $res[\'merge\'] = true; } if (!$out = @fopen($uploadPath, "wb")) { return $this->message(1004, \'文件不可写\'); } while ($buff = fread($in, 4096)) { fwrite($out, $buff); } @fclose($in); @fclose($out); $res[\'code\'] = 0; return $res; }
前端是通过multipart/form-data;
将文件以二进制形式传给PHP,所以我们用$_FILES
接收文件信息。
接收到文件后,我们判断这个文件是否就只有1个分片,如果只有1个分片就没必要再合成了,直接将该分片保存到files/下,并且告诉前端不需要合并文件:$res[\'merge\'] = false;
。
如果是有多个分片,那就将这些分片保存到临时目录下,分片的命名应该是“文件唯一标识_当前分片”,如abcd_1,标识文件abcd的第一个分片,这样我们接下来合并文件就好办了。
合并之前,先检查下该文件的所有分片是否都上传完毕,就是检测分片文件是否都存在。
public function merge() { $filePath = self::$tmpDir. DIRECTORY_SEPARATOR . $this->fileInfo[\'identifier\']; $totalChunks = $this->fileInfo[\'totalChunks\']; //总分片数 $filename = $this->fileInfo[\'filename\']; //文件名 $done = true; //检查所有分片是否都存在 for ($index = 1; $index message(1005, \'分片信息错误\'); } //如果所有文件分片都上传完毕,开始合并 $timeStart = $this->getmicrotime(); //合并开始时间 $saveDir = self::$saveDir . DIRECTORY_SEPARATOR . date(\'Y-m-d\'); if (!is_dir($saveDir)) { @mkdir($saveDir); } $uploadPath = $saveDir . DIRECTORY_SEPARATOR .$filename; if (!$out = @fopen($uploadPath, "wb")) { return $this->message(1004, \'文件不可写\'); } if (flock($out, LOCK_EX) ) { // 进行排他型锁定 for($index = 1; $index getmicrotime(); //合并完成时间 $res[\'code\'] = 0; $res[\'time\'] = $timeEnd - $timeStart; //合并总耗时 return $res; }
如果分片文件都存在,开始合并所有分片,现将要最终合并的文件锁定,然后遍历所有分片,将分片文件依次写入合并的文件中,最后释放锁定。
每个分片被合并后,应当立即删除该分片。
这里我测试用了计算合并过程的耗时,真实应用可以将计时代码去掉。
我用自己的机器测试(8G内存,SSD),上传了一个约800MB的文件,2M一个分片,约400个分片,合并总耗时3秒钟,合并一个3G的文件耗时30秒钟。也就是说文件越大,分片越多,合成文件所花费的时间越长。但是通过观察内存变化,上面的代码在合并文件时内存消耗很低。那如果是特别大的文件,就会有大量分片,那这样的话合并过程是不是很耗时耗性能呢?
对于特大号的文件合并,有人提出建立一套算法,一个文件有N个分片,先建立一个序列,序列分成N个片段,每个分片占用一个片段,文件上传时就把对应的分片塞到对应的片段中,最终分片文件上传完了文件也就合成好了。这个方法也不错,将合并的时间分摊到每个分片上传上去了。
还有人提出,使用追加的方式将分片一片片往文件里塞,整个方法不可取,因为如果设置并发数大的话,不能保证文件是否按分片顺序合成的,最终有可能得到的文件是个乱序的不可用的文件。
那么我给大家建议使用Swoole来处理文件合成这一步,让耗时的操作在后台运行,不让前端等待,悄悄的在后台合成文件即可,如何?
up.php用来实例化Uploader上传类,接收前端请求,并且获取相关参数实例化Uploader后,分别调用上传分片、合并文件和检测文件方法。
fileInfo = [ \'filename\' => htmlentities($data[\'filename\']), //文件名称 \'identifier\' => htmlentities($data[\'identifier\']), //文件唯一标识 \'totalSize\' => intval($data[\'totalSize\']), //文件总大小 \'totalChunks\' => intval($data[\'totalChunks\']) //总分片数 ]; $res = $up->merge(); } else { $method = $_SERVER[\'REQUEST_METHOD\']; if ($method === \'POST\') { //上传 $up->fileInfo = [ \'identifier\' => htmlentities($_POST[\'identifier\']), //每个文件的唯一标识 \'filename\' => htmlentities($_POST[\'filename\']), //文件名称 \'totalSize\' => intval($_POST[\'totalSize\']), //文件总大小 \'chunkNumber\' => intval($_POST[\'chunkNumber\']), //当前是第几个分片 \'totalChunks\' => intval($_POST[\'totalChunks\']) //总分片数 ]; $res = $up->upload(); } else { //上传前检测文件md5和分片 $res = $up->checkFile(); } } echo json_encode($res);
本文地址:https://www.stayed.cn/item/281
转载请注明出处。
本站部分内容来源于网络,如侵犯到您的权益,请 联系我