基于JS与PHP的分片上传与断点续传
转载文章,尚未验证
对于目前大文件上传,较为合理的方式就是分片上传。get方式仅可以传输几K的数据,POST 理论上是可以传输无限的数据,但会有服务器的限制,php.ini
里面的upload_max_filesize
会对你的文件上传数据大小加以限制,一旦超过这个数值,服务器就会拒绝接收,所以前端要进行大文件分割,分成文件块进行上传。
前端JS实现
对于分片上传,最重要的就是slice方法,此方法可以分割文件。
fileBlock = file.slice(start, end)
, fileBlock
即为大小为(end – start)分割好的文件块。
对于上传,不能使用json进行传输数据,而要使用formData进行传输,也就类似于直接点击form表单的submit的按钮进行上传。
因为是前端是基于vue写的,会有一些变量不在方法中,在方法中会进行解释。
我是基于axios封装的ajax进行上传
分片上传
将上传文件封装成一个方法
function uploadSlice (file, skip) { const blockSize = 1024 * 1024 // 一个文件块的大小为1M let totalNum = Math.ceil(file.size / blockSize) // 可分为多少文件块 let formData = new FormData() // 不用json包装数据,而使用formData let config = { headers = { 'Content-Type': 'multipart/form-data' // 将请求头的Content-Type改为这个,后端才能接收到上传的文件块的二进制流 } } let nextSize = Math.min((skip + 1) * blockSize, file.size) let fileData = file.slice(skip * blockSize, nextSize) // fileData就是分割好的blob对象,可进行二进制流传输 formData.append('file', fileData) // append方法可以在formData对象进行数据追加 formData.append('blobNum', skip + 1) // blobNum就是目前上传到第几块,以便后端判断是否上传完成 formData.append('totalNum', totalNum) formData.append('id', this.id) // id为后端存储记录对应的id axios.post('upload/slice', formData, config).then({ ({ code }) => { if (code === 2) { this.uploadSlice(file, ++skip) } else if (code === 1) { alert('上传成功') } } }) }
断点续传
上传期间会有各种不可违逆因素导致上传中断,因此断点续传是不可或缺的。
断点续传主要分为两步:第一步,向后端获取中断的节点,此时需要验证用户再次选择的文件是否与上传中断的文件一样;第二步,在中断节点处进行分片上传。
验证文件并获取节点
我写的时候其实是带token验证的,但是基于普适性,就没将token加上去了
function compareFile () { const blockSize = 1024 * 1024 let formData = new FormData() let config = { headers: { 'Content-Type': 'multipart/form-data' } } // file是用户选择文件后生成的File对象,blobNum是向后端获取的中断节点 let fileData = this.file.slice((this.blobNum - 1) * blockSize, Math.min(this.blobNum * blockSize, this.file.size)) formData.append('file', fileData) axios.post('upload/check', formData, config).then({ ({ result }) => { if (result) { this.sliceUpload(this.file, this.blobNum) // 验证成功,基于中断节点进行分片上传 }else { alert('选择的文件与已上传不一致') } } } }) }
后端PHP实现
后端是基于TP开发的,会使用一些TP封装的方法。
后端主要是接收前端发送的各个文件块,然后当前端上传结束后,后端要将这些文件块重新合并成原文件,并将相应信息存储到数据库中。
Upload上传类
这个类是用于上传接收与合并的核心类,封装了一些文件操作的方法。
网上合并用的是file_put_contents
,这个方法很水泵内存,其实直接使用PHP的文件读写操作就可以完成。
class Upload { private $filePath; // 上传目录 private $tmpFile; // 临时文件 private $blobNum; // 当前文件块 private $totalBlobNum; // 总共文件块 private $fileName; // 文件名 private $file; // 文件 public function __construct($tmpFile, $blobNum, $totalBlobNum, $fileName, $where) { $this->filePath = $where; !is_dir($this->filePath) && mkdir($this->filePath, 0777, true); $this->tmpFile = $tmpFile->getInfo()['tmp_name']; $this->blobNum = $blobNum; $this->totalBlobNum = $totalBlobNum; $this->fileName = $fileName; $this->file = $this->filePath . DS . $fileName; $this->fileMerge(); } // 文件合并 private function fileMerge() { $tmpFile = fopen($this->file . '__tmp', 'a+'); fwrite($tmpFile, file_get_contents($this->tmpFile)); fclose($tmpFile); if ($this->blobNum === $this->totalBlobNum) rename($this->file . '__tmp', $this->file); if (file_exists($this->file)) if (file_exists($this->file . '__tmp')) @unlink($this->file . '__tmp'); } public function result() { $data = 0; if ($this->blobNum === $this->totalBlobNum) { if (file_exists($this->filePath . DS . $this->fileName)) { $data = 1; } } else { if (file_exists($this->filePath . DS . $this->fileName . '__tmp')) { $data = 2; } } if ($data === 0) // 这个方法是我封装的,用于抛出HttpException,TP会自动处理这个Exception,并返回给前端 SeverResponse::error('文件上传出错'); return $data; } }
断点续传的文件检验
public static function compareFile($uploadPath, $fileName, $tmp, $blobNum = 1) { $blockSize = 1024 * 1024; $tmpPath = $uploadPath . DS . 'tmp'; !is_dir($tmpPath) && mkdir($tmpPath, 0777, true); $tmpFilePath = $tmpPath . DS . $fileName . '__tmp'; $tmpFile = fopen($tmpFilePath, 'a+'); fwrite($tmpFile, file_get_contents($fileName . '__tmp', false, null, ($blobNum - 1) * $blockSize, $blockSize)); fclose($tmpFile); // md5_file方法可以用来检验文件一致性,FileUtil::deleteFile是用于删除文件的方法 if (md5_file($tmpFilePath) === md5_file($tmp->getInfo()['tmp_name'])) { FileUtil::deleteFile($tmpFilePath); return true; } else { FileUtil::deleteFile($tmpFilePath); return false; } }
接收分片上传的controller
public function upload() { // request中TP封装的处理HTTP请求的助手函数 $file = request()->file('file'); $data = [ 'blobNum' => request()->param('blobNum'), 'totalNum' => request()->param('totalNum'), 'id' => request()->param('id') ]; // $info为通过id获取相应纪录对应的对象 $upload = new Upload($file, $info->blob_num + 1, $info->total_num, CheckAndGenerate::getFileName($info->source), 'uploadPath'); $result = $upload->result(); if ($result === 2) { // 正在上传的状态 // 分片上传中,可以将对应blobNum存入数据库,以便进行断点续传 // 此处用redis纪录blobNum进行优化更好 } if ($result === 1) { // 上传完成的状态 // 上传完成后,对数据库进行一系列操作 } // 此方法是我封装用于返回json对象 return SeverResponse::getSuccessMessage(['code' => $result]); }
这篇文章是我开发中遇到的一个技术难点,将大部分的逻辑处理分享出来,其中很多细节部分我就没往上写了,希望这篇文章能帮到一些人吧。
本文由 猫斯基 转载发布。
转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。本网转载其他媒体之稿件,意在为公共提供免费服务。如稿件版权单位或个人不想再本网发布,可与本网联系,核实后将立即将其删除。 本文地址:https://www.maosiji.com/php-duandianxuchuan.html
转载地址:网络、