PHP搭建大文件切割分块上传功能示例


Posted in PHP onJanuary 04, 2017

背景

在网站开发中,文件上传是很常见的一个功能。相信很多人都会遇到这种情况,想传一个文件上去,然后网页提示“该文件过大”。因为一般情况下,我们都需要对上传的文件大小做限制,防止出现意外的情况。
 但是在有些业务场景中,大文件上传又是必须的,比如邮箱附件,或者内部OA等等。

问题

服务端为什么不能直接传大文件?跟php.ini里面的几个配置有关

upload_max_filesize = 2M //PHP最大能接受的文件大小
post_max_size = 8M //PHP能收到的最大POST值'
memory_limit = 128M //内存上限
max_execution_time = 30 //最大执行时间

当然不能简单粗暴的把上面几个值调大,否则服务器内存资源吃光是迟早的问题。

解决思路

好在HTML5开放了新的FILE API,也可以直接操作二进制对象,我们可以直接在浏览器端实现文件切割,按照以前的做法就得用Flash的方案,实现起来会麻烦很多。

JS思路

1.监听上传按钮的onchange事件

2.获取文件的FILE对象

3.把文件的FILE对象进行切割,并且附加到FORMDATA对象中

4.把FORMDATA对象通过AJAX发送到服务器

5.重复3、4步骤,直到文件发送完。

PHP思路

1.建立上传文件夹

2.把文件从上传临时目录移动到上传文件夹

3.所有的文件块上传完成后,进行文件合成

4.删除文件夹

5.返回上传后的文件路径

DEMO代码

前端部分代码

<!doctype html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport"
   content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <title>Document</title>
 <style>
  #progress{
   width: 300px;
   height: 20px;
   background-color:#f7f7f7;
   box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);
   border-radius:4px;
   background-image:linear-gradient(to bottom,#f5f5f5,#f9f9f9);
  }

  #finish{
   background-color: #149bdf;
   background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);
   background-size:40px 40px;
   height: 100%;
  }
  form{
   margin-top: 50px;
  }
 </style>
</head>
<body>
<div id="progress">
 <div id="finish" style="width: 0%;" progress="0"></div>
</div>
<form action="./upload.php">
 <input type="file" name="file" id="file">
 <input type="button" value="停止" id="stop">
</form>
<script>
 var fileForm = document.getElementById("file");
 var stopBtn = document.getElementById('stop');
 var upload = new Upload();

 fileForm.onchange = function(){
  upload.addFileAndSend(this);
 }

 stopBtn.onclick = function(){
  this.value = "停止中";
  upload.stop();
  this.value = "已停止";
 }

 function Upload(){
  var xhr = new XMLHttpRequest();
  var form_data = new FormData();
  const LENGTH = 1024 * 1024;
  var start = 0;
  var end = start + LENGTH;
  var blob;
  var blob_num = 1;
  var is_stop = 0
  //对外方法,传入文件对象
  this.addFileAndSend = function(that){
   var file = that.files[0];
   blob = cutFile(file);
   sendFile(blob,file);
   blob_num += 1;
  }
  //停止文件上传
  this.stop = function(){
   xhr.abort();
   is_stop = 1;
  }
  //切割文件
  function cutFile(file){
   var file_blob = file.slice(start,end);
   start = end;
   end = start + LENGTH;
   return file_blob;
  };
  //发送文件
  function sendFile(blob,file){
   var total_blob_num = Math.ceil(file.size / LENGTH);
   form_data.append('file',blob);
   form_data.append('blob_num',blob_num);
   form_data.append('total_blob_num',total_blob_num);
   form_data.append('file_name',file.name);

   xhr.open('POST','./upload.php',false);
   xhr.onreadystatechange = function () {
    var progress;
    var progressObj = document.getElementById('finish');
    if(total_blob_num == 1){
     progress = '100%';
    }else{
     progress = Math.min(100,(blob_num/total_blob_num)* 100 ) +'%';
    }
    progressObj.style.width = progress;
    var t = setTimeout(function(){
     if(start < file.size && is_stop === 0){
      blob = cutFile(file);
      sendFile(blob,file);
      blob_num += 1;
     }else{
      setTimeout(t);
     }
    },1000);
   }
   xhr.send(form_data);
  }
 }

</script>
</body>
</html>

PHP部分代码

<?php
class Upload{
 private $filepath = './upload'; //上传目录
 private $tmpPath; //PHP文件临时目录
 private $blobNum; //第几个文件块
 private $totalBlobNum; //文件块总数
 private $fileName; //文件名

 public function __construct($tmpPath,$blobNum,$totalBlobNum,$fileName){
  $this->tmpPath = $tmpPath;
  $this->blobNum = $blobNum;
  $this->totalBlobNum = $totalBlobNum;
  $this->fileName = $fileName;
  
  $this->moveFile();
  $this->fileMerge();
 }
 
 //判断是否是最后一块,如果是则进行文件合成并且删除文件块
 private function fileMerge(){
  if($this->blobNum == $this->totalBlobNum){
   $blob = '';
   for($i=1; $i<= $this->totalBlobNum; $i++){
    $blob .= file_get_contents($this->filepath.'/'. $this->fileName.'__'.$i);
   }
   file_put_contents($this->filepath.'/'. $this->fileName,$blob);
   $this->deleteFileBlob();
  }
 }
 
 //删除文件块
 private function deleteFileBlob(){
  for($i=1; $i<= $this->totalBlobNum; $i++){
   @unlink($this->filepath.'/'. $this->fileName.'__'.$i);
  }
 }
 
 //移动文件
 private function moveFile(){
  $this->touchDir();
  $filename = $this->filepath.'/'. $this->fileName.'__'.$this->blobNum;
  move_uploaded_file($this->tmpPath,$filename);
 }
 
 //API返回数据
 public function apiReturn(){
  if($this->blobNum == $this->totalBlobNum){
    if(file_exists($this->filepath.'/'. $this->fileName)){
     $data['code'] = 2;
     $data['msg'] = 'success';
     $data['file_path'] = 'http://'.$_SERVER['HTTP_HOST'].dirname($_SERVER['DOCUMENT_URI']).str_replace('.','',$this->filepath).'/'. $this->fileName;
    }
  }else{
    if(file_exists($this->filepath.'/'. $this->fileName.'__'.$this->blobNum)){
     $data['code'] = 1;
     $data['msg'] = 'waiting for all';
     $data['file_path'] = '';
    }
  }
  header('Content-type: application/json');
  echo json_encode($data);
 }
 
 //建立上传文件夹
 private function touchDir(){
  if(!file_exists($this->filepath)){
   return mkdir($this->filepath);
  }
 }
}

//实例化并获取系统变量传参
$upload = new Upload($_FILES['file']['tmp_name'],$_POST['blob_num'],$_POST['total_blob_num'],$_POST['file_name']);
//调用方法,返回结果
$upload->apiReturn();

存在的问题

这只是一个简单的DEMO,有很多地方需要改进,比如上传的文件夹与临时文件放在一起,用户中途取消也没有发请求进行清理,容易造成文件冗余。JS采用的是同步模型,文件需要一块一块按顺序上传,会导致整个浏览器在上传的过程中出于堵塞的状态,按了按钮可能需要几秒钟才能反应过来,用户体验不好。真正需要产品化的时候就要综合考虑多种情况,当然作为一个示例,引导大家了解分块上传的思路还是不错的。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

PHP 相关文章推荐
文件上传类
Oct 09 PHP
真正面向对象编程:PHP5.01发布
Oct 09 PHP
木翼下载系统中说明的PHP安全配置方法
Jun 16 PHP
smarty section简介与用法分析
Oct 03 PHP
PHP Memcached + APC + 文件缓存封装实现代码
Mar 11 PHP
PHP MemCached高级缓存配置图文教程
Aug 05 PHP
php mssql扩展SQL查询中文字段名解决方法
Oct 15 PHP
关于PHP堆栈与列队的学习
Jun 21 PHP
php多种形式发送邮件(mail qmail邮件系统 phpmailer类)
Jan 22 PHP
基于命令行执行带参数的php脚本并取得参数的方法
Jan 25 PHP
PHP版微信第三方实现一键登录及获取用户信息的方法
Oct 14 PHP
PHP基于phpqrcode类生成二维码的方法详解
Mar 14 PHP
php实现的简单中文验证码功能示例
Jan 03 #PHP
php与c 实现按行读取文件实例代码
Jan 03 #PHP
浅谈PHP安全防护之Web攻击
Jan 03 #PHP
php中遍历二维数组并以表格的形式输出的方法
Jan 03 #PHP
解析PHP之提取多维数组指定列的方法
Jan 03 #PHP
PHP实现RTX发送消息提醒的实例代码
Jan 03 #PHP
php cookie用户登录的详解及实例代码
Jan 03 #PHP
You might like
DISCUZ在win2003环境下 Unable to access ./include/common.inc.php in... 的问题终极解决方案
2011/11/21 PHP
smarty半小时快速上手入门教程
2014/10/27 PHP
php浏览历史记录的方法
2015/03/10 PHP
IE与Firefox下javascript getyear年份的兼容性写法
2007/12/20 Javascript
Extjs学习笔记之六 面版
2010/01/08 Javascript
javascript简易缓动插件(源码打包)
2012/02/16 Javascript
JS对象转换为Jquery对象示例
2014/01/26 Javascript
自己使用jquery写的一个无缝滚动的插件
2014/04/30 Javascript
多种js图片预加载实现方式分享
2016/02/19 Javascript
JavaScript操作选择对象的简单实例
2016/05/16 Javascript
AngularJs解决跨域问题案例详解(简单方法)
2016/05/19 Javascript
jQuery筛选数组之grep、each、inArray、map的用法及遍历json对象
2016/06/20 Javascript
微信开发 JS-SDK 6.0.2 经常遇到问题总结
2016/12/08 Javascript
jQuery实现滚动条滚动到子元素位置(方便定位)
2017/01/08 Javascript
Bootstrap进度条学习使用
2017/02/09 Javascript
前端页面文件拖拽上传模块js代码示例
2017/05/19 Javascript
vue bootstrap小例子一枚
2017/06/09 Javascript
从零撸一个pc端vue的ui组件库( 计数器组件 )
2019/08/08 Javascript
VUE实现强制渲染,强制更新
2019/10/29 Javascript
python使用PythonMagick将jpg图片转换成ico图片的方法
2015/03/26 Python
python动态网页批量爬取
2016/02/14 Python
利用Python批量生成任意尺寸的图片
2016/08/29 Python
用python简单实现mysql数据同步到ElasticSearch的教程
2018/05/30 Python
python用plt画图时,cmp设置方法
2018/12/13 Python
python绘制雪景图
2019/12/16 Python
Python基础类继承重写实现原理解析
2020/04/03 Python
Python爬虫之Selenium多窗口切换的实现
2020/12/04 Python
Notino芬兰:购买香水和化妆品
2019/04/15 全球购物
护理人员的自我评价分享
2014/03/15 职场文书
梅花魂教学反思
2014/04/25 职场文书
银行竞聘演讲稿
2014/05/16 职场文书
2014年教师党员公开承诺书
2014/05/28 职场文书
教您怎么制定西餐厅运营方案 ?
2019/07/05 职场文书
职场领导同事生日简短祝福语
2019/08/06 职场文书
如何将numpy二维数组中的np.nan值替换为指定的值
2021/05/14 Python
SpringBoot生成License的实现示例
2021/06/16 Java/Android