一文搞懂分片上传和断点续传
Posted autofelix
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文搞懂分片上传和断点续传相关的知识,希望对你有一定的参考价值。
〝 古人学问遗无力,少壮功夫老始成 〞
一文搞懂分片上传和断点续传,对于做过文件上传的小伙伴对于这两个名词并不太陌生,而在上传大文件的业务中,这两种上传方式是经常被用到的,但是很多小白对这两种上传的方法的用法还是有点模糊,如果这篇文章能给你带来一点帮助,希望给飞兔小哥哥一键三连,表示支持,谢谢各位小伙伴们。
目录
一、什么是分片上传、断点续传
- 分片上传:将一个文件切割为一系列特定大小的小数据片,分别将这些小数据片分别上传到服务端,全部上传完后再在服务端将这些小数据片合并成为一个完整的资源。
- 断点续传:在分片上传的过程中,如果因为系统崩溃或者网络中断等异常因素导致上传中断,这时候客户端需要记录上传的进度。在之后支持再次上传时,可以继续从上次上传中断的地方进行继续上传
- 在网络不好,文件很大的情况下可以采用分片上传
- 而因为网络崩溃或者系统中断,可以采用断点续传
二、项目地址
- 该项目已经传到github和gitee上,欢迎大家fork、star、watch三连支持
- github项目地址:https://github.com/autofelix/autofelix-javascript-wiki/tree/master/fcup
- gitee项目地址:https://gitee.com/autofelix/autofelix-javascript-wiki/tree/master/fcup
三、分片上传实战
- 将完整视频切成每一个小片
- 这里我们使用的是jquery.fcup官方切片库
- 完整源代码都在上面的项目地址中
- 前端我们只要引入该切片库,进行以下简单的配置即可
$.fcup({
upId: 'upid', //上传dom的id
upShardSize: '1', //切片大小,(单次上传最大值)单位M,默认2M
upMaxSize: '200', //上传文件大小,单位M,不设置不限制
upUrl: 'fcup.php', //文件上传接口
upType: 'jpg,png,jpeg,gif,mp4', //上传类型检测,用,号分割
//接口返回结果回调,根据结果返回的数据来进行判断,可以返回字符串或者json来进行判断处理
upCallBack: function (res) {
var status = res.status;
var msg = res.msg;
// 已上传完成
if (res.status == 2) {
console.log(msg);
}
// 还在上传中
if (status == 1) {
console.log(msg);
}
// 接口返回错误
if (status == 0) {
// 停止上传并且提示信息
$.upStop(msg);
}
},
// 上传过程监听,可以根据当前执行的进度值来改变进度条
upEvent: function (num) {
// num的值是上传的进度,从1到100
console.log('当前上传进度:' + num + '%');
},
// 发生错误后的处理
upStop: function (errmsg) {
// 这里只是简单的alert一下结果,可以使用其它的弹窗提醒插件
// alert(errmsg);
},
// 开始上传前的处理和回调,比如进度条初始化等
upStart: function () {
alert('开始上传');
}
});
四、如何进行切片
- 在切片库中我们可以看到以下流程
- 第一步:加载配置、获取dom元素、执行fcup_addFileInput方法
- 第二步:其实就是获取上传dom元素的位置然后在该位置加一个透明的文件上传按钮,这样用户在点击dom元素的时候,其实点击的是这个透明的文件上传按钮,弹出上传文件的选项
- 第三步:第二步其实已经初始化完毕,后续操作需要用户点击,触发fcup_upload上传方法
- 第四步:进行切片上传核心代码,设置分片大小、切片,递归通过ajax进行上传
//这是第一步
fcup: function (config) {
jQuery.extend(config);
if (jQuery.upId && jQuery.upUrl) {
jQuery.domhtml = jQuery('#' + jQuery.upId).html();
jQuery.fcup_addFileInput();
}
},
//这是第二步
fcup_addFileInput: function () {
jQuery.upInputId = jQuery.upId + '_input';
var C = jQuery('#' + jQuery.upId).attr("class");
var X = jQuery('#' + jQuery.upId).position().top;
var Y = jQuery('#' + jQuery.upId).position().left;
var W = jQuery('#' + jQuery.upId).innerWidth();
var H = jQuery('#' + jQuery.upId).innerHeight();
var html = jQuery.domHtml;
if (C) {
html += '<input type="file" id="' + jQuery.upInputId + '" class="' + C + '" onchange="jQuery.fcup_upload()" style="position:absolute;left:' + Y + 'px;top:' + X + 'px;opacity:0;z-index:9999;width:' + W + 'px;height:' + H + 'px;">';
} else {
html += '<input type="file" id="' + jQuery.upInputId + '" onchange="jQuery.fcup_upload()" style="position:absolute;left:' + Y + 'px;top:' + X + 'px;opacity:0;z-index:9999;width:' + W + 'px;height:' + H + 'px;">';
}
jQuery('#' + jQuery.upId).html(html);
},
//这是第三步
fcup_upload: function () {
var result = '';
jQuery.upError = '';
var file = jQuery('#' + jQuery.upInputId)[0].files[0];
if (!file) {
return false;
}
if (typeof jQuery.upStart == 'function') {
jQuery.upStart();
}
name = file.name;
size = file.size;
index1 = name.lastIndexOf(".");
if (!jQuery.upShardSize) {
jQuery.upShardSize = 2;
}
var fileMD5 = jQuery.get_file_md5(file);
var index2 = name.length,
suffix = name.substring(index1 + 1, index2),
shardSize = jQuery.upShardSize * 1024 * 1024,
succeed = 0,
shardCount = Math.ceil(size / shardSize);
if (jQuery.upType) {
uptype = jQuery.upType.split(",");
if (jQuery.inArray(suffix, uptype) == -1) {
jQuery.upError = '不允许上传的文件类型-' + suffix;
}
}
if (jQuery.upMaxSize) {
if (!jQuery.fcup_limitFileSize(file, jQuery.upMaxSize + 'MB')) {
jQuery.upError = '上传文件过大';
}
}
if (jQuery.upStatus() == false) {
return false;
}
setInterval("jQuery.fcup_upFileInput();",500);
var re = [];
var start, end = 0;
for (var i = 0; i < shardCount; ++i) {
re[i] = [];
start = i * shardSize,
end = Math.min(size, start + shardSize);
re[i]["file_data"] = file.slice(start, end);
re[i]["file_name"] = name;
re[i]["file_size"] = size; //文件大小
}
const URL = jQuery.upUrl;
var i2 = 0, i3 = 1, fcs = Array();
var xhr = new XMLHttpRequest();
function ajaxStack(stack) {
if (jQuery.upStatus() == false) {
return;
}
var form = new FormData();
if (stack[i2]) {
fcs = stack[i2];
form.append("file_data", fcs['file_data']);
form.append("file_name", fcs['file_name']);
form.append("file_size", fcs['file_size']);
form.append("file_md5", fileMD5);
form.append("file_total", shardCount);
form.append("file_index", i3);
xhr.open('POST', URL, true);
xhr.onload = function () {
ajaxStack(stack);
}
xhr.onreadystatechange = function () {
if (jQuery.upStatus() == false) {
return;
}
if (xhr.readyState == 4 && xhr.status == 200) {
var data = xhr.responseText ? eval('(' + xhr.responseText + ')') : '';
++succeed;
var cent = jQuery.fcup_getPercent(succeed, shardCount);
if (typeof jQuery.upEvent == 'function') {
jQuery.upEvent(cent);
}
if (cent == 100) {
setTimeout(function () {
if (typeof jQuery.upCallBack == 'function') {
jQuery.upCallBack(data);
}
}, 500);
} else {
if (typeof jQuery.upCallBack == 'function') {
jQuery.upCallBack(data);
}
}
}
}
xhr.send(form);
i2++; i3++;
form.delete('file_data');
form.delete('file_name');
form.delete('file_size');
form.delete('file_md5');
form.delete('file_total');
form.delete('file_index');
}
}
ajaxStack(re);
re = null,
file = null;
},
//这是核心代码
for (var i = 0; i < shardCount; ++i) {
re[i] = [];
start = i * shardSize,
end = Math.min(size, start + shardSize);
re[i]["file_data"] = file.slice(start, end);
re[i]["file_name"] = name;
re[i]["file_size"] = size; //文件大小
}
五、后端对分片的处理
- 因为每个分片的md5值是唯一的
- 所以可以通过md5值判断分片文件是否已经上传
- 如果已经上传可以直接跳过,否则进行分片的保存
- 通过索引和总数进行对比判断是否上传成功
- 上传成功的话,将分片进行合并成一个完整的文件
- 因为服务器内存的最大化,文件合并完毕就可以将分片删除清理
private function run($file)
{
//上传分片到分片目录
$shard_file = $file['file_md5'] . DIRECTORY_SEPARATOR . $file['file_index'];
//跳过重复文件
$result = file_exists($shard_file) ?: move_uploaded_file($file['file_data']['tmp_name'], $shard_file);
if ($this->isFinish($file) && $result) {
try {
for ($i = 0; $i < $file['file_total']; $i++) {
$fileIndex = $i + 1;
$blob = file_get_contents($file['file_md5'] . DIRECTORY_SEPARATOR . $fileIndex);
file_put_contents($this->saveRoot . DIRECTORY_SEPARATOR . $file['file_name'], $blob, FILE_APPEND);
//删除分片文件
array_map('unlink', glob("{$file['file_md5']}/{$fileIndex}"));
}
//删除分片文件夹
@rmdir($file['file_md5']);
$this->jsonBack([
'status' => static::UPLOAD_FINISH,
'msg' => '上传完成'
]);
} catch (Exception $e) {
$this->jsonBack([
'status' => static::UPLOAD_ERROR,
'msg' => $e->getMessage()
]);
}
} else {
if ($result) {
$this->jsonBack([
'status' => static::UPLOAD_START,
'msg' => '正在上传:' . $file['file_index'] . '/' . $file['file_total']
]);
} else {
$this->jsonBack([
'status' => static::UPLOAD_ERROR,
'msg' => '上传错误,请重新上传'
]);
}
}
}
六、断点续传
- 其实在上面流程已经可以显示出来了
- 上传的时候有个索引值,告诉你,当前已经传到哪一个分片了
- 如果系统崩溃,导致上传断开,可以判断这个索引值,上传过的可以略过,进行后续的上传
以上是关于一文搞懂分片上传和断点续传的主要内容,如果未能解决你的问题,请参考以下文章