基于JS与PHP的分片上传与断点续传

作者猫斯基 2022-08-31 217 人读过

最新文章

转载文章,尚未验证

对于目前大文件上传,较为合理的方式就是分片上传。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://maosiji.com/php/php-duandianxuchuan.html

转载地址:网络