最近公司项目需要用到断点续传,所以记录一下其中的坑
使用到的主要技术
webuploader
thinkphp5
断点续传的思路:
客户端:
1.获取文件md5(MD5是文件唯一标识,用来判断是否存在此文件,并且用作分片的文件夹名)
2.将文件分片
3.验证分片是否上传过,上传过直接跳过当前分片
3.上传分片到md5的文件夹(保存文件名建议按分片序号来,因为分片的顺序很重要)
4.最后一个分片上传完成后发送合并分片请求并由服务器返回文件信息
服务端
1.获取md5文件夹下的文件数量并返回用作分片验证
2.接收文件分片并保存到文件md5的文件夹,文件名称使用分片序号:如0.mp4,1.mp4
3.合并分片时将md5文件夹下的所有文件按文件名顺序提取并写入到最终的文件内
4.写入完成获取最终文件hash并判断是否存在,存在则返回已存在文件,删除当前文件,不存在则写入数据库并返回文件信息
大体思路是这样,实际还要加入许多验证,比客户端获取到md5后马上要验证文件是否存在,存在就不上传,直接使用文件信息,不存在则上传
分片上传前也要验证,不过分片的跳过规则需要注意,服务器只需要返回已有的分片数量,客户端根据已有的分片和当前分片索引即可判断是否应该跳过,因为分片是按顺序上传的,如:
,服务端需要注意合并的时候顺序不能乱,顺序乱了就无法还原源文件,所以建议用分片索引作为文件名,获取的时候直接按0,1,2这样获取就行了.
并且保存时要注意保存在自定义的目录下需要确保该目录存在,不存在的目录需要新建后才可以保存文件
完整代码如下:
客户端:
var uploader;
var fileMd5;
var is_upload;
var totalFiles;
function initWebUploader() {
/***************************************************** 监听分块上传过程中的三个时间点 start ***********************************************************/
WebUploader.Uploader.register({
“before-send”:“beforeSend”, //每个分片上传前
},
{
//时间点2:如果有分块上传,则每个分块上传之前调用此函数
beforeSend:function(block){
var deferred = WebUploader.Deferred();
if(is_upload)//跳过到开始上传的哪一个分片时
{
deferred.resolve();
}else if(totalFiles)//已经获取过文件数量则直接判断是否跳过
{
//当前分片下标小于等于目录下的文件数量就认为分块存在,因为分块上传下标是由小到大
if (block.chunk > totalFiles - 1) {
is_upload = true;
deferred.resolve();
} else {
//分块存在,跳过
deferred.reject();
}
} else {
KaTeX parse error: Expected 'EOF', got '}' at position 689: …); }̲ re…maccms.path}static/webupload/Uploader.swf’,
// 文件接收服务端。
server: ‘/api.php/upload/chunkUpload’,
// 选择文件的按钮。可选。
// 内部根据当前运行是创建,可能是input元素,也可能是flash.
pick: ‘#name’,
accept: {
title: ‘Images’,
extensions: ‘mp4’,
},
// 不压缩image, 默认如果是jpeg,文件上传前会压缩一把再上传!
resize: false,
prepareNextFile:true,
chunked : true, // 分片处理
chunkSize : 5 * 1024 * 1024, // 每片50M,经过测试,发现上传1G左右的视频大概每片50M速度比较快的,太大或者太小都对上传效率有影响
chunkRetry : false,// 如果失败,则不重试
threads:1,
formData:{guid:WebUploader.Base.guid()}
});
uploader.on(‘fileQueued’,function (file) {
uploader.md5File( file,0,1010241024 )
// 及时显示进度
.progress(function(percentage) {
console.log(‘Percentage:’, percentage);
$(“.readFile”).text(“正在读取视频信息…”+(percentage * 100).toFixed(2) + ‘%’);
})
// 完成
.then(function(val) {
console.log(‘md5 result:’, val);
$(“.readFile”).text(‘’);
fileMd5 = val;
var formData = uploader.option(‘formData’);
formData.md5 = val;
uploader.option(‘formData’,formData);
//验证文件是否存在
$.post(‘/api.php/upload/md5check’,{md5:val},function (data) {
if(data.code)
{
//选择后直接上传
uploader.upload();
$(“#name .webuploader-pick”).text(file.name);
$(“.up-state .file_name”).text(file.name);
var size;
size = Math.round(file.size/(1024*1024),2);
if(size > 1024)
{
size = Math.round(size/1024,2) + ‘G’;
}else{
size += ‘M’;
}
$(“.up-state .file_size”).text(size);
$(‘.up-in’).width(‘0’);
$(‘.bar’).find(‘span’).html(‘0%’);
$(‘.up-con’).hide();
$(‘.up-state’).show();
$(‘#go’).click(function () {
$(‘.up-end’).hide();
$(‘.up-con’).show();
})
}else{
var msg = ‘该视频已存在!’;
alert(msg);
uploader.reset();
$(“#name .webuploader-pick”).text(file.name);
$(“.up-state .file_name”).text(file.name);
$(“.file-address input
name=vodplayurl
“).val(data.data.path);
$(”.file-address input
name=urlid
“).val(data.data.id);
//获取视频时长,配合video标签
$(”#duration”).attr(“src”,data.data.path);
}
});
});
});
// 文件上传过程中创建进度条实时显示。
uploader.on( ‘uploadProgress’, function( file, percentage ) {
$(‘.bar’).find(‘span’).html((percentage * 100).toFixed(2) + ‘%’);
$(‘.up-in’).width(percentage * 100 + ‘%’);
});
uploader.on( ‘uploadSuccess’, function( file,res ) {
//最后一块完成时间
//全部上传完成发送合并请求
$.post(‘/api.php/upload/videoUpload’,{md5:fileMd5},function (data) {
if(data.code)
{
$(‘.up-end’).show();
$(‘.up-state’).hide();
setTimeout(function () {
$(“.up-end”).hide();
$(‘.up-con’).show();
},2000);
$(“.file-address input
name=vodplayurl
“).val(data.data.path);
$(”.file-address input
name=urlid
“).val(data.data.id);
$(”#duration”).attr(“src”,data.data.path);
}else{
(“.up-end h1”).text(‘上传出错’);
$(‘.up-end’).show();
$(‘.up-state’).hide();
setTimeout(function () {
$(“.up-end”).hide();
$(‘.up-con’).show();
},2000);
}
});
});
uploader.on( ‘uploadError’, function( file ) {
$(“.up-end h1”).text(‘上传出错’);
$(‘.up-end’).show();
$(‘.up-state’).hide();
setTimeout(function () {
$(“.up-end”).hide();
$(‘.up-con’).show();
},2000);
});
}
服务器:
//分片上传
function chunkUpload($name = ‘file’)
{
$md5 = request()->param()
′md5′
;
o
b
j
e
c
t
i
n
f
o
=
r
e
q
u
e
s
t
(
)
−
>
f
i
l
e
(
object_info = request()->file(
objectinfo=request()−>file(name);
//保存文件的顺序很重要!
$object = $object_info->rule(‘uniqid’)->move(PATH_FILE .
m
d
5
.
′
/
′
,
r
e
q
u
e
s
t
(
)
−
>
p
a
r
a
m
(
)
′
c
h
u
n
k
′
)
;
i
f
(
md5 . '/',request()->param() ′chunk′ ); if(
md5.′/′,request()−>param()′chunk′);if(object)
{
return
′chunks′=>request()−>param()[′chunks′
,‘chunk’=>request()->param()
′chunk′
];
}else{
return false;
}
}
//最终合并文件
function videoUpload()
{
$md5 = request()->param()
′md5′
;
$dir = PATH_FILE .
m
d
5
;
i
f
(
i
s
d
i
r
(
md5; if(is_dir(
md5;if(isdir(dir)) {
//获取文件的顺序很重要!!!
$files =
;
$chunk_id = 0;
$chunk_file = PATH_FILE .
m
d
5
.
′
/
′
;
w
h
i
l
e
(
f
i
l
e
e
x
i
s
t
s
(
md5 . '/'; while (file_exists(
md5.′/′;while(fileexists(chunk_file . $chunk_id . ‘.mp4’)){
$files
= $chunk_file . $chunk_id . ‘.mp4’;
$chunk_id++;
}
$file_name = randomStr().‘.mp4’;
$path_file = date(‘Ymd’) . SYS_DS_PROS . $file_name;
//日期目录不存在则创建目录
if(!is_dir(PATH_FILE . date(‘Ymd’)))
{
mkdir(PATH_FILE . date(‘Ymd’));
}
c
o
u
n
t
=
0
;
f
o
r
e
a
c
h
(
count = 0; foreach (
count=0;foreach(files as $v)
{
f
i
l
e
=
f
i
l
e
g
e
t
c
o
n
t
e
n
t
s
(
_file = file_get_contents(
file=filegetcontents(v);
$_res = file_put_contents(PATH_FILE .
p
a
t
h
f
i
l
e
,
path_file,
pathfile,_file,FILE_APPEND);
if(KaTeX parse error: Expected '}', got 'EOF' at end of input: … unlink(v);
}else{
KaTeX parse error: Expected 'EOF', got '}' at position 22: …+; }̲ } …count == 0)
{
rmdir($dir);
//检查合并后的文件hash
$_hash = hash_file(‘sha1’, PATH_FILE . $path_file);
//合并后的文件已存在则删除已合并文件并返回已有文件信息
$file_info =
t
h
i
s
−
>
m
o
d
e
l
F
i
l
e
−
>
g
e
t
I
n
f
o
(
′
s
h
a
1
′
=
>
this->modelFile->getInfo( ′sha1′=>
this−>modelFile−>getInfo(′sha1′=>hash
,‘id,name,path,sha1,guid,md5’);
if(!empty($file_info))
{
unlink(PATH_FILE . $path_file);
return $file_info;
}
//合并后的文件入库并返回
d
a
t
a
=
′
n
a
m
e
′
=
>
_data = ′name′=>
data=′name′=>filename,′path′=>
p
a
t
h
f
i
l
e
,
′
s
h
a
1
′
=
>
pathfile,′sha1′=>
pathfile,′sha1′=>hash,′guid′=>′′,′md5′=>request()−>param()[′md5′
];
$result =
t
h
i
s
−
>
m
o
d
e
l
F
i
l
e
−
>
a
d
d
I
n
f
o
(
this->modelFile->addInfo(
this−>modelFile−>addInfo(_data);
$_data
′id′
= $result;
return $_data;
}else{
$this->error = ‘合并文件失败’;
return false;
}
}else{
$this->error = ‘分片目录不存在!’;
return false;
}
}
//分片验证
function checkChunk()
{
$md5 = request()->param()
′md5′
;
$dir = PATH_FILE .
m
d
5
;
i
f
(
i
s
d
i
r
(
md5; if(is_dir(
md5;if(isdir(dir)) {
$files =
t
h
i
s
−
>
g
e
t
F
i
l
e
B
y
P
a
t
h
(
this->getFileByPath(
this−>getFileByPath(dir);
return count($files);
}else{
return false;
}
}
=========================================================================
最新更新:
分片验证存在bug,当上传分片不小心删除了前面的分片时就导致无法合成文件(文件数量导致跳过了),因此,更新分片验证
前端:
//时间点2:如果有分块上传,则每个分块上传之前调用此函数
beforeSend:function(block){
var deferred = WebUploader.Deferred();
if(is_upload)//跳过到开始上传的哪一个分片时
{
deferred.resolve();
}else if(totalFiles)//已经获取过文件数量则直接判断是否跳过
{
//当前分片下标小于等于目录下的文件数量就认为分块存在,因为分块上传下标是由小到大
if (!totalFiles.in_array(block.chunk)) {
deferred.resolve();
} else {
//分块存在,跳过
deferred.reject();
}
} else {
$.post(‘/api.php/upload/checkUpload’, {md5: fileMd5}, function (data) {
if (data.code) {
totalFiles = data.data;
//当前分片下标小于等于目录下的文件数量就认为分块存在,因为分块上传下标是由小到大
if (!data.data.in_array(block.chunk)) {
deferred.resolve();
} else {
//分块存在,跳过
deferred.reject();
}
} else {
is_upload = true;
deferred.resolve();
}
});
}
return deferred.promise();
后端:
//分片验证
public function checkChunk()
{
$md5 = request()->param()
′md5′
;
$dir = PATH_FILE .
m
d
5
;
i
f
(
i
s
d
i
r
(
md5; if(is_dir(
md5;if(isdir(dir)) {
$files =
t
h
i
s
−
>
g
e
t
F
i
l
e
B
y
P
a
t
h
(
this->getFileByPath(
this−>getFileByPath(dir);
foreach ($files as
k
=
>
k=>
k=>v)
{
f
i
l
e
n
a
m
e
=
e
x
p
l
o
d
e
(
′
/
′
,
file_name = explode('/',
filename=explode(′/′,v);
$file_name =
f
i
l
e
n
a
m
e
c
o
u
n
t
(
file_name count(
filenamecount(filename)−1
;
f
i
l
e
i
n
d
e
x
=
e
x
p
l
o
d
e
(
′
.
′
,
file_index = explode('.',
fileindex=explode(′.′,file_name)
0
;
$files
k
=
(
i
n
t
)
k = (int)
k=(int)file_index;
}
return $files;
}else{
return false;
}
}
利用文件名的有序性判断当前分片索引是否存在服务器
判断是否存在数组中的函数:
Array.prototype.in_array = function (element) {
for (var i = 0; i < this.length; i++) {
if (this
i
== element) {
return true;
}
} return false;
};
参考文章:http://blog.ncmem.com/wordpress/2023/10/27/php%e7%bb%93%e5%90%88webuploader%e6%96%ad%e7%82%b9%e7%bb%ad%e4%bc%a0%e7%9a%84%e5%ae%9e%e7%8e%b0/
欢迎入群一起讨论