基于php+webuploader的大文件分片上传,支持断点续传,带进度条

发布时间 2023-09-19 11:43:13作者: Xproer-松鼠

基于php+webuploader的大文件分片上传,带进度条,支持断点续传(刷新、关闭页面、重新上传、网络中断等情况)。文件上传前先检测该文件是否已上传,如果已上传提示“文件已存在”,如果未上传则直接上传。视频上传时会根据设定的参数(分片大小、分片数量)进行上传,上传过程中会在目标文件夹中生成一个临时文件夹,用于存储临时分片,等所有分片上传完毕后,会根据序号重新组合成一个完整的视频,临时文件被删除。

如果文件上传至七牛云,可参看基于php大文件分片上传至七牛云,带进度条

首先下载webuploader

效果图:

 

临时文件,用于存储分片

 html代码

<title>webuploader分片上传</title>
<meta charset="utf8">
<!--引入CSS-->
<link rel="stylesheet" type="text/css" href="/static/webupload/webuploader.css">
<script type="text/javascript" src="/static/index/js/jquery.js"></script>
<script type="text/javascript" src="/static/index/js/jquery.md5.js"></script>

<!--引入JS-->
<script type="text/javascript" src="/static/webupload/webuploader.js"></script>
<div id="uploader" class="wu-example">
<!--用来存放文件信息-->
<div id="thelist" class="uploader-list"></div>
<div class="btns">
<div id="picker">选择文件</div>
<button id="ctlBtn" class="btn btn-default">开始上传</button>
</div>
</div>
<style>
.progress{
height: 20px; width: 300px; background: #ccc; }
.progress-bar{
height: 20px; background: #0a3536;}
</style>
<script>

var uploadswf = '/static/webupload/Uploader.swf';
var chunkSize = 2*1024*1024;
var server_url='uploadVedio';
var GUID = WebUploader.Base.guid();//一个GUID
var chunkObj = {}; //用来记录文件的状态、上传中断的位置
var seq=1;
var msg='';
$(function () {
var $ = jQuery;
var $list = $('#thelist');
WebUploader.Uploader.register({
"before-send-file":"beforeSendFile",
"before-send": "beforeSend"
}, {
"beforeSendFile": function (file) {
//上传前校验文件是否已经上传过
var deferred = WebUploader.Deferred();
$.ajax({
type:"POST",
//上传前校验文件上传到第几片
url: "checkFile",
data: {
seq: seq,
fileMd5: $.md5(file.name + file.size + file.ext),
fileName:file.name
},
dataType: "json",
success: function (data) {
console.log(data);
chunkObj = data;
chunkObj.type = data.type;
chunkObj.chunk == data.chunk;
msg = data.msg;
if(data.type==404){
deferred.reject();
$("#" + file.id).find(".state").text(data.msg);
}else if (data.type == 0) {
deferred.reject();
$("#" + file.id).find(".state").text("文件已上传");
} else if (data.type == 1) {
if (data.chunk) {
deferred.resolve();
}
} else {
deferred.resolve();
}

},
error: function () {
deferred.resolve();
}
})
//deferred.resolve();
return deferred.promise();
},
"beforeSend": function (block) {
var deferred = WebUploader.Deferred();
var curChunk = block.chunk;
var totalChunk = block.chunks;
console.log(chunkObj)
if (chunkObj.type == "1") {
if (curChunk < chunkObj.chunk) {
deferred.reject();
} else {
deferred.resolve();
}
} else {
deferred.resolve();
}
return deferred.promise();
}
});
var uploader = WebUploader.create({
// 选完文件后,是否自动上传。
auto: false,
// swf文件路径
swf: uploadswf,
// 文件接收服务端。
server: server_url,
// 内部根据当前运行是创建,可能是input元素,也可能是flash.
pick: '#picker',
chunked: true,//开始分片上传
chunkSize:1 * 1024 * 1024,//每一片的大小
threads: 1,
formData: {
},

// 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
resize: false
});

// 当有文件被添加进队列的时候
uploader.on('fileQueued', function (file) {

$list.append('<div id="' + file.id + '" class="item">' +
'<div class="item-file"><div class="fileType-logo"></div>' +
'<div class="fileMes"><h4 class="info">' + file.name + '</h4>' +
'<p class="state">等待上传...</p>' +
'</div></div></div>');

});
// 文件上传过程中创建进度条实时显示。
uploader.on('uploadProgress', function (file, percentage) {
var $li = $('#' + file.id),
$percent = $li.find('.progress .progress-bar');
// 避免重复创建
if (!$percent.length) {
$percent = $('<div class="progress progress-striped active">' +
'<div class="progress-bar" role="progressbar" style="width: 0%">' +
'</div>' +
'</div>').appendTo($li).find('.progress-bar');
}
$li.find('p.state').text('上传中');
$percent.css('width', percentage * 100 + '%');
});
uploader.on("uploadBeforeSend", function (obj, data, headers) {
var file = obj.cuted.file;


data.test = 1;
data.fileMd5 = $.md5(file.name + file.size + file.ext);


})
// 文件上传成功,给item添加成功class, 用样式标记上传成功。
uploader.on('uploadSuccess', function (file, response) {
if(response.status==299){
$('#' + file.id).find('p.state').text('文件已存在');
}else{
$('#' + file.id).find('p.state').text('已上传');
}

});
// 文件上传失败,显示上传出错。
uploader.on('uploadError', function (file) {
$('#' + file.id).find('p.state').text(msg);
});
// 完成上传完了,成功或者失败,先删除进度条。
uploader.on('uploadComplete', function (file) {
$('#' + file.id).find('.progress').fadeOut();
});
//所有文件上传完毕
uploader.on("uploadFinished", function () {
//提交表单
});
//开始上传
$("#ctlBtn").click(function () {
uploader.upload();

});
});

</script>

php请求后端

// 引用封装好的Upload类
use app\index\controller\Upload;
public function uploadVedio()
{
$model =new Upload();
$res = $model->doUpload();
$model->ajaxReturn($res);

}

封装上传类

<?php
namespace app\index\controller;
use think\Controller;

/**
* 大文件分片上传
*/
class Upload extends Controller
{


private $filepath = 'uploads/'; //上传目录
private $blobNum; //第几个文件块
private $totalBlobNum; //文件块总数
private $fileName; //文件名
#允许上传的文件
private $allowExtension = ['mp4','avi','wmv'];
#文件后缀
private $fileExtension ='';
#当前块内容
private $nowFile = '';
#文件大小
private $totalSize = 0;
#文件总大小只允许1G
private $allowFileSize = 0;
#文件md5 前端传过来的 用于创建临时文件夹 上传完后删除
private $fileMd5='';

public function __construct($savePath =''){
$postData = $_POST;
#测试断点上传
if(isset($postData['test'])){
sleep(1);
}
if($savePath){
$this->filepath = $this->filepath.$savePath;
}
# #文件名称
# var_dump($postData);
$postData['name'] =isset($postData['name'])?$postData['name']:'';
$this->fileName =$postData['name'];
if($this->isHaveFile()){
$this->ajaxReturn(['status'=>299,'msg'=>'文件已存在!']);
}

$this->fileMd5 =$postData['fileMd5'];

#允许文件的大小 1G
$this->allowFileSize =(1*1024*1024*1024);

if((int)$postData['size']>$this->allowFileSize){
$this->ajaxReturn(['status'=>204,'msg'=>"文件大小超1G限制!"]);
}
#文件大小
$this->totalSize=$postData['size'];
$postData['chunks']=isset($postData['chunks'])?(int)$postData['chunks']:1;
$postData['chunk']=isset($postData['chunk'])?(int)$postData['chunk']:0;
if(!(int)$postData['chunks']){
$this->ajaxReturn(['status'=>208,'msg'=>'chunks参数错误']);
}

#当前块
$this->blobNum =$postData['chunk']+1;
#总共块
$this->totalBlobNum =$postData['chunks'];

#获取后缀
$fileExtension =explode(".",basename( $this->fileName));
$this->fileExtension=array_pop($fileExtension);
#检测后缀是否在允许范围
$this->checkFileExtension();
$this->nowFile = $_FILES['file'];
if( $this->nowFile['error'] > 0) {
$msg['status'] = 502;
$msg['msg'] = "文件错误!";
$this->ajaxReturn($msg);
}

}
public function doUpload(){
#临时文件移动到指定目录下
$res = $this->moveFile();
if($res['status']==999){
return $this->fileMerge();
}else{
return $res;
}
}

#创建md5 文件名
public function createFileName(){
return $this->filepath.$this->fileName;
}

#检测文件是否重复
public function isHaveFile(){
if(file_exists($this->filepath.$this->fileName)){
return true;
}
return false;
}
#文件合并
public function fileMerge(){
if ($this->blobNum == $this->totalBlobNum) {
$fileName = $this->createFileName();
@unlink($fileName);
#删除旧文件
#文件合并 文件名以
$handle=fopen($fileName,"a+");
for($i=1; $i<= $this->totalBlobNum; $i++){
#当前分片数
$this->blobNum = $i;
#吧每个块的文件追加到 上传的文件中
fwrite($handle,file_get_contents($this->createBlockFileName()));
}
fclose($handle);
#删除分片
for($i=1; $i<= $this->totalBlobNum; $i++){
$this->blobNum = $i;
@unlink($this->createBlockFileName());
}
#删除临时目录
@rmdir($this->filepath.$this->fileMd5);
if(filesize($fileName) == $this->totalSize){
$msg['status'] = 200;
$msg['msg'] = '上传成功!';
$msg['size'] = $this->totalSize;
$msg['filename'] = "http://".$_SERVER['HTTP_HOST']."/".$this->createFileName();
$msg['name'] = $this->fileName;
}else{
$msg['status'] = 501;
$msg['msg'] = '上传文件大小和总大小有误!';
@unlink($this->createFileName());
}
return $msg;
# $this->ajaxReturn($msg);
}
}
#检测上传类型
public function checkFileExtension(){
if(!in_array(strtolower($this->fileExtension),$this->allowExtension)){
$this->ajaxReturn(['status'=>203,'msg'=>"文件类型不允许"]);
}
}
#将临时文件移动到指定目录
public function moveFile(){
try{
#每个块的文件名 以文件名的MD5作为命名
$filename=$this->createBlockFileName();
#分片文件写入
$handle=fopen($filename,"w+");
fwrite($handle,file_get_contents($this->nowFile ['tmp_name']));
fclose($handle);
#不是最后一块就返回当前信息 是最后一块往下执行合并操作
if($this->blobNum != $this->totalBlobNum) {
$msg['status'] = 201;
$msg['msg'] = "上传成功!";
$msg['blobNum'] = $this->blobNum;
return $msg;
#$this->ajaxReturn($msg);
}else{
$msg['status'] = 999;
$msg['msg'] = "上传成功!";
$msg['blobNum'] = $this->blobNum;
return $msg;
}
}catch (Exception $e){
$msg['status'] = 501;
$msg['error'] = $e->getMessage();
return $msg;
#$this->ajaxReturn($msg);
}
}
#创建分片文件名
public function createBlockFileName(){
$dirName = $this->filepath.$this->fileMd5."/";
if (!is_dir($dirName) ) {
@mkdir($dirName, 0700);
};
return $dirName.$this->blobNum.".part";
}

#json格式放回处理
public function ajaxReturn($msg){
exit(json_encode($msg));
}
}

参考文章:http://blog.ncmem.com/wordpress/2023/09/19/%e5%9f%ba%e4%ba%8ephpwebuploader%e7%9a%84%e5%a4%a7%e6%96%87%e4%bb%b6%e5%88%86%e7%89%87%e4%b8%8a%e4%bc%a0%ef%bc%8c%e6%94%af%e6%8c%81%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%ef%bc%8c%e5%b8%a6%e8%bf%9b/

欢迎入群一起讨论