应用截图:
前端部分
<!DOCTYPE html>
<html>
<head>
<title>PHP大文件分片上传</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<style type="text/css">
#percent-bg{position:relative;width:100%;height:20px;border:1px solid #ccc;}
#percent{position:absolute;display:block;width:0;height:100%;left:0;background:green;z-index:100;}
#percent_num{position:absolute;display:block;width:30px;height:100%;left:50%;margin-left:-15px;z-index:200; text-align:center;}
</style>
<script type="text/javascript" src="jquery-3.3.1.min.js"></script>
<script type="text/javascript">
var pz = {
"maxSize":1024*1024*200, //文件大小限制20M
"bufferSize":1024*512, //单个分片大小
"blocks":[], //分片数据集合
"blockName":"newfile",//表单 file 的名字
"filename":"",//上传的本地文件真实文件名
"threadNum":2,//上传线程数量
"reset":function(fname){
this.blocks = [];
this.filename = fname;
}
}
function afterselect(){
if(document.getElementById("x").files.length<1){
return false;
}
var file = document.getElementById("x").files[0];
console.log(file);
if(file.size>pz.maxSize){
showmsg("文件大小限制:"+pz.maxSize+",实际文件大小:"+file.size);
return;
}
pz.reset(file.name);
__index = __activeThreadCount = __sendedBlockCount = 0;
var endByte = 0,startByte = 0;
while(true){
startByte = endByte;
if(endByte + pz.bufferSize >= file.size){
endByte = file.size;
}else{
endByte = endByte+pz.bufferSize;
}
var block = sliceFile(file,startByte,endByte);
if(!block){
showmsg("分片失败");
return;
}
pz.blocks.push(block);
if(endByte >= file.size){ break; }
}
showmsg("文件名:"+file.name);
showmsg("文件大小:"+file.size);
showmsg("总分片数量:"+pz.blocks.length);
var dis = Math.min(pz.threadNum,pz.blocks.length);
for(var i=0;i<dis;i++){
(function(i){
setTimeout(function(){
__activeThreadCount++;
fenpei(i);
},500);
})(i);
}
showmsg("启动线程:"+dis+" 个");
showmsg("------------------------");
}
//只要是操作主线程中的变量或其他,就一定会自动同步到主线程来。相当于自动lock()
var __index = 0;
var __activeThreadCount = 0; //当前活跃线程数量
var __sendedBlockCount = 0; //已上传的block数量
function fenpei(i){
//参数:线程名
if(__index>=pz.blocks.length){
showmsg("线程"+i+' 结束');
__activeThreadCount--;
if(__activeThreadCount==0){
showmsg("------------------------");
showmsg('多线程分片上传完毕,正在处理分片数据...');
combineFile();
}
return;
}
uploadBlock(i,__index);
__index++;
}
//发送拼接分块命令
function combineFile(){
$.post(
'http://www.exhibitioncloud.com/api/v1/Uploadfile/up_video',
{"act":"combine","location":"12","chunks":pz.blocks.length,"filename":pz.filename},
function(e){
if(e.data.flag){
showmsg("分片数据处理完成,任务结束,URl:"+e.data.url);
}else{
showmsg(e.data.info);
}
},'json'
);
}
//显示进度
function showPercent(){
var percent = parseInt(__sendedBlockCount / pz.blocks.length *100);
if(percent>100){ percent = 100; }
$("#percent").stop(true,true).animate({"width":percent+"%"},10);
console.log(percent);
$("#percent_num").html(percent+"%");
}
//上传一个分片
function uploadBlock(i,index){
var chunk = index;
showmsg('线程'+i+' 分片'+chunk+' start');
var fd = new FormData();
fd.append("newfile", pz.blocks[chunk]);//文件
fd.append("filename", pz.filename);//文件名
fd.append("chunk", chunk);//分片索引
fd.append("chunks", pz.blocks.length);//分片总数
$.ajax({
url:'http://www.exhibitioncloud.com/api/v1/Uploadfile/up_video',
type:"post",
data: fd,
dataType:"json",
cache: false,
contentType:false,
processData:false,//设置为true的时候,jquery ajax 提交的时候不会序列化 data,而是直接使用data
success:function(data){
//timer线程中的异步,此时同步了
showmsg("线程"+i+" 分片"+chunk+" end");
__sendedBlockCount++;
showPercent();
fenpei(i);
},
complete:function(XMLHttpRequest,textStatus){
},
error:function(){}
});
}
//分割file成分片
function sliceFile(file,startByte,endByte){
if(file.slice){
return file.slice(startByte,endByte);
}
if(file.webkitSlice){
return file.webkitSlice(startByte,endByte);
}
if(file.mozSlice){
return file.mozSlice(startByte,endByte);
}
return null;
}
//输出消息
function showmsg(msg){
var _t = $("#msg").val();
_t += msg+"\r\n";
$("#msg").val(_t);
var scrollTop = $("#msg")[0].scrollHeight;
$("#msg").scrollTop(scrollTop);
}
</script>
<body>
<input type="file" id="x" >
<button onclick="afterselect();">dianji</button>
<textarea id="msg" style="width:99%;height:400px;overflow-y:auto;"></textarea>
<p></p>
<div id="percent-bg">
<span id="percent"></span>
<span id="percent_num">0%</span>
</div>
</body>
</html>
PHP接口部分
use app\common\utility\Uploadfile as Uploadfiles
/**
* @title 视频文件上传
* @description 视频文件上传接口说明
* @author 毕昌远
* @url /api/v1/Uploadfile/up_video
* @method post
*
* @header name:user-token require:1 default: desc:token
*
* @param name:newfile type:file require:0 default: other:form文件 desc:form文件
* @param name:filename type:string require:0 default: other:文件名称 desc:文件名称
* @param name:chunk type:int require:0 default: other:当前块数 desc:当前块数
* @param name:chunks type:int require:0 default: other:总块数 desc:总块数
* @param name:act type:string require:0 default: other:combine合并 desc:combine合并
* @param name:loaction type:string require:0 default: other:文件存放地址 desc:文件存放地址
*
* @return flag:状态
* @return info:信息体
* @return url:文件存放位置
*/
public function up_video()
{
$res = array(
'code' => 200,
'message'=>"",
'data' =>[
'flag' =>false,
'url' =>'',//上传得到的新路径
'info' =>'',
'msg' =>''
]
);
// 组织上传参数
$inout_data = [
'act' => input('act'),
'chunk' => input('chunk'),
'chunks' => input('chunks'),
'filename' => input('filename'),
'location' => input('location'),
'formdataname' => 'newfile',
"file_path" => ''
];
// 文件后缀数组
$file_type_arr = array("mp4");
// 获取文件后缀
$file_type = substr(strrchr(input("filename"), '.'), 1);
// 监测文件后缀
$file_type_status = in_array($file_type, $file_type_arr);
if (!$file_type_status) {
$res['data']['msg']="文件类型有误";
echo json_encode($res); die();
}
// 文件存放地址
$inout_data['file_path'] = 'uploads/up_video';
$Uploader = new Uploadfiles();
$arr = $Uploader->uploads($inout_data);
if(empty($arr[0])){
$res['data']['info'] = $arr[1];
}else{
if ($arr[0]==2) {
$pathurl = "http://".$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'];
$pathurl = substr($pathurl,0,strripos($pathurl,"/"));
$pathurl = "";
$res['data']['flag'] = false;
$res['data']['url'] = $pathurl.'/'.$arr[1];
$res['data']['info'] = "success";
}else{
$pathurl = "http://".$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'];
$pathurl = substr($pathurl,0,strripos($pathurl,"/"));
$pathurl = "";
$res['data']['flag'] = true;
$res['data']['url'] = $pathurl.'/'.$arr[1];
$res['data']['info'] = "success";
}
}
echo json_encode($res); die();
}
接口引用类
<?php
namespace app\common\utility;
session_start();
//文件分片上传
class Uploadfile{
//执行上传(的name属性,保存路径-相对当前路径)
public function uploads($data){
$savedir = $data['file_path'];
$return_arr = array('0','');
$_SESSION['uploader_id']=1;
$userID = $_SESSION['uploader_id']; //用户标识
if(!empty($data['act']) && trim($data['act'])=='combine'){
//-------------合并文件
$chunks = intval($data['chunks']);//总分块个数
//文件后缀
$type = substr($data['filename'],strripos($data['filename'],'.'));
//保存临时文件
$tmppath = $savedir.'/tmp'; //临时目录,
if(!file_exists($tmppath)){ @mkdir($tmppath,0777,true); }
$filenamemd5 = md5($data['filename']);
$savedir = $savedir.'/'.date('Ymd',time());
// 文件存放地址
// $savedir = $data['location'];
if(!file_exists($savedir)){
@mkdir($savedir,0777,true);
}
$newname = date('mdH',time()).rand(10000,99999).'_'.rand(100000,999999).$type;
// 文件保存的名称
// $newname = $data['filename'];
$writer = fopen("{$savedir}/{$newname}","ab"); //合并后的文件名
for($i=0;$i<$chunks;$i++){
$file2read = "{$tmppath}/{$userID}_{$filenamemd5}_{$i}";
$reader = fopen($file2read,"rb");
fwrite($writer,fread($reader,filesize($file2read)));
fclose($reader);
unset($reader);
@unlink($file2read);//删除分块临时文件
}
fclose($writer);
$return_arr[0]='1';
$return_arr[1]="{$savedir}/{$newname}";
}else{
if(empty($_FILES[$data['formdataname']]) || $_FILES[$data['formdataname']]["error"] > 0){
return array( '0','上传失败' );
}
//-------------保存临时文件
$chunks = intval($data['chunks']);//总分块个数
$chunk = intval($data['chunk']);//当前分块索引
//临时目录
$tmppath = $savedir.'/tmp';
if(!file_exists($tmppath)){
@mkdir($tmppath,0777,true);
}
$filenamemd5 = md5($data['filename']);
$tmpname = "{$userID}_{$filenamemd5}_{$chunk}";//临时文件名
@move_uploaded_file($_FILES[$data['formdataname']]["tmp_name"],"{$tmppath}/{$tmpname}");
$return_arr[0]='2';
$return_arr[1]='needmore';
}
return $return_arr;
}
}