流程:
配置:
这里按照上传文件最大2G来配置的,根据自己的需要来做修改!
Nginx: PHP:
前端:
这里以 layui 为例 Html: 使用 sparkmd5 获取文件内容的md5,用于保证上传文件的唯一性。
<script src="__STATIC__/admin/js/sparkmd5/sparkmd5.js?v={$version}"></script>
<div class="layui-input-block layui-upload">
<button type="button" class="layui-btn layui-btn-normal" id="chooseList">选择文件</button>
<div class="layui-upload-list">
<table class="layui-table">
<colgroup>
<col width="30%">
<col width="10%">
<col width="30%">
<col width="30%">
</colgroup>
<thead>
<tr><th>文件名</th>
<th>大小</th>
<th>上传进度</th>
<th>操作</th>
</tr></thead>
<tbody id="uploadList"></tbody>
</table>
</div>
</div>
页面效果:
JS:
layui.use(['form', 'upload', 'element'], function () {
var form = layui.form;
var upload = layui.upload;
var element = layui.element;
var $ = layui.$;
var progressTimer = 211;
element.render('progress');
upload.render({
elem: '#chooseList',
elemList: $('#uploadList'),
url: '../Ajax/multiupload',
accept: 'file',
auto: false,
choose: function (obj) {
var data = this.data;
var files = this.files = obj.pushFile();
var partSize = 1024 * 1024 * 2;
for (var key in files) {
if ($('#upload-'+key).val() == undefined) {
var fileName = files[key].name;
var fileExt = fileName.substr(fileName.lastIndexOf('.') + 1);
fileName = fileName.substr(0, fileName.lastIndexOf('.'));
var that = this;
var file_size = (files[key].size / 1024).toFixed(2) + 'KB';
if (files[key].size > 1000 * 1000) {
file_size = (files[key].size / 1024 / 1024).toFixed(2) + 'MB';
}
if (files[key].size > 1000 * 1000 * 1000) {
file_size = (files[key].size / 1024 / 1024 / 1024).toFixed(2) + 'GB';
}
var tr = $(['<tr id="upload-' + key + '">'
, '<td id="file-name-' + key + '">' + files[key].name + '</td>'
, '<td>' + file_size + '</td>'
, '<td><div class="layui-progress" lay-filter="progress-demo-' + key + '"><div class="layui-progress-bar" lay-percent=""></div></div></td>'
, '<td>'
, '<input type="hidden" class="status" id="status-' + key + '" value="1"/>'
, '<input type="hidden" id="part-num-' + key + '" value="1"/>'
, '<input type="hidden" id="upload-id-' + key + '" value=""/>'
, '<input type="hidden" id="contents-' + key + '" value=""/>'
, '<input type="hidden" id="part-total-' + key + '" value=""/>'
, '<span class="layui-icon layui-icon-loading-1 layui-anim layui-anim-rotate layui-anim-loop" id="icon-loop-' + key + '"></span>'
, '<span class="layui-icon layui-icon-ok-circle" hidden id="icon-ok-' + key + '"></span>'
, '<span class="layui-icon layui-icon-close" hidden id="icon-close-' + key + '"></span>'
, '<a class="layui-btn layui-btn-xs layui-btn-danger" id="upload-cancel-' + key + '">取消</a>'
, '<a class="layui-btn layui-btn-xs re-upload" id="re-upload-' + key + '">重试</a>'
, '<a class="layui-btn layui-btn-normal layui-btn-xs" id="set-title-' + key + '">生成标题</a></span>'
, '</td>'
, '</tr>'].join(''));
that.elemList.append(tr);
tr.find('#re-upload-' + key).on('click', function () {
$('#re-upload-'+key).hide();
$('#status-' + key).val('1');
getmd5(files[key], partSize).then(e => {
if (e != undefined) {
multiupload(progressTimer, data, files[key], partSize, key, fileName, fileExt, e, obj);
}
})
});
tr.find('#upload-cancel-' + key).on('click', function () {
$('#status-'+key).val('-1');
$('#upload-' + key).remove();
delete files[key];
layer.msg('已删除该队列!');
});
tr.find('#set-title-' + key).on('click', function () {
$("#title").val($('#file-name-' + key).html());
});
$('.re-upload').hide();
getmd5(files[key], partSize).then(e => {
if (e != undefined) {
multiupload(progressTimer, data, files[key], partSize, key
, fileName, fileExt, e, obj);
}
})
}
}
},
done: function (res) {
if (res.status == 1) {
var page = res.part_num;
var totalPage = res.part_total;
element.progress('progress-demo-'+res.upload_index, Math.ceil(page * 100 / totalPage) + '%');
page = parseInt(page) + 1;
$('#part-num-'+res.upload_index).val(page);
$('#status-'+res.upload_index).val(res.status);
$('#upload-id-'+res.upload_index).val(res.upload_id);
$('#contents-'+res.upload_index).val(res.contents);
$('#part-total-'+res.upload_index).val(res.part_total);
$('#icon-loop-'+res.upload_index).show();
$('#icon-close-'+res.upload_index).hide();
} else if (res.status == 2) {
$('#upload-cancel-' + res.upload_index).on('click', function () {
$.ajax({
type: 'get',
url: './delFile',
data: {
doc_id: res.uploadfile_id
},
success: function () {
layer.msg('上传已取消!');
}
})
});
element.progress('progress-demo-'+res.upload_index, '100%');
$('#status-'+res.upload_index).val(res.status);
$('#re-upload-'+res.upload_index).hide();
$('#icon-ok-'+res.upload_index).show();
$('#icon-loop-'+res.upload_index).hide();
layer.msg('上传成功');
delete this.files[res.upload_index];
} else {
this.error(res.upload_index);
}
},
error: function (index) {
$('#icon-loop-'+index).hide();
$('#icon-close-'+index).show();
$('#status-'+index).val('-1');
$('.re-upload').show();
$('.status[value=2]').siblings('.re-upload').hide();
layer.alert("上传失败,请检查网络后重试");
}
});
});
function multiupload(progressTimer,data,file,partSize,key,fileName,fileExt,hash_name,obj) {
progressTimer = setInterval(function () {
var page = parseInt($('#part-num-'+key).val());
var status = $('#status-'+key).val();
if (parseInt(status) == 2 || parseInt(status) == -1) {
clearInterval(progressTimer);
} else {
if (status == 1) {
$('#status-'+key).val('0');
data.file_name = fileName;
data.part_num = page;
data.file_ext = fileExt;
data.upload_index = key;
data.file_size = file.size;
data.hash_name = hash_name;
obj.upload(key, file.slice((page - 1) * partSize, page * partSize));
}
}
}, 100);
}
function getmd5(file, chunkSize) {
return new Promise((resolve, reject) => {
let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;
let chunks = Math.ceil(file.size / chunkSize);
let currentChunk = 0;
let spark = new SparkMD5.ArrayBuffer();
let fileReader = new FileReader();
fileReader.onload = function(e) {
spark.append(e.target.result);
currentChunk++;
if (currentChunk < chunks) {
loadNext();
} else {
let md5 = spark.end();
resolve(md5);
}
};
fileReader.onerror = function(e) {
reject(e);
};
function loadNext() {
let start = currentChunk * chunkSize;
let end = start + chunkSize;
if (end > file.size){
end = file.size;
}
fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
}
loadNext();
});
}
上传中效果图 上传成功效果图
上传失败效果图
PHP:
获取页面传过来的参数生成文件的md5名称。
$data['upload_name'] = md5($data['hash_name'].$data['file_name'].$data['file_ext'].$data['file_size'].$data['user_id']);
把上传的分片文件保存到本地,注意框架封装的获取上传文件的详细参数方法可能会出错。
查询上传日志记录表是否存在该上传文件分片的记录。
$upload_log = $system_upload_log->getDataByUploadName($data['upload_name'],$data['part_num']);
如果有该分片记录,则返回最新的一条记录,否则返回空数组。
public function getDataByUploadName($upload_name,$part_num){
$data = $this->where(['upload_name' => $upload_name,'part_num' => $part_num])->select()->toArray();
return $data ? $this->where(['upload_name' => $upload_name])->order('id desc')->find()->toArray() : $data;
}
如果有记录,则代表是续传,返回前端当前文件的上传记录
if ($upload_log){
$data['status'] = $upload_log['status'];
$data['upload_id'] = $upload_log['upload_id'];
$data['contents'] = $upload_log['contents'];
$data['part_num'] = $upload_log['part_num'];
$data['part_total'] = $upload_log['part_total'];
return ['save' => true,'data' => $data];
}
一些判断
$data['upload_id'] = $data['upload_id'] ? $data['upload_id'] : '';
$data['contents'] = $data['contents'] ? $data['contents'] : date('Ymd');
$data['part_total'] = $data['part_total'] ? $data['part_total'] : ceil($data['file_size']/(1024*1024*2));
$data['part_size'] = $data['part_num'] * (1024*1024*2) > $data['file_size']
? $data['file_size'] - (($data['part_num']-1) * (1024*1024*2)) : (1024*1024*2);
if ($data['part_num'] == $data['part_total']){
$data['status'] = 2;
$data['etag'] = $system_upload_log->getEtagByUploadName($data['upload_name']);
}
$object = 'upload/'.$data['contents'].'/'.$data['upload_name'].'.'.$data['file_ext'];
在OssClient.php文件写一个新方法,参考Oss本身的分片上传方法 multiuploadFile
public function multiupload($bucket, $object, $file,$options = null)
{
$this->precheckCommon($bucket, $object, $options);
if (empty($file)) {
throw new OssException("parameter invalid, file is empty");
}
$uploadFile = OssUtil::encodePath($file);
if (!isset($options[self::OSS_CONTENT_TYPE])) {
$options[self::OSS_CONTENT_TYPE] = $this->getMimeType($object, $uploadFile);
}
if (!$options['upload_id']) {
$options['upload_id'] = $this->initiateMultipartUpload($bucket, $object,
[ 'Content-Type' => $options[self::OSS_CONTENT_TYPE],
'partSize' => $options['part_size']
]);
}
$up_options = array(
self::OSS_FILE_UPLOAD => $uploadFile,
self::OSS_PART_NUM => $options['part_num'],
self::OSS_SEEK_TO => 0,
self::OSS_LENGTH => $options['part_size'],
self::OSS_CHECK_MD5 => false,
);
$options['part_num'] <= $options['part_total'] && $response_upload_part = $this->uploadPart($bucket, $object, $options['upload_id'], $up_options);
if (isset($options['etag'])){
$options['etag'][] = substr($response_upload_part,1,-1);
}else{
$options['etag'] = substr($response_upload_part,1,-1);
}
if ($options['status'] == 2){
$uploadParts = array();
foreach ($options['etag'] as $i => $value) {
$uploadParts[] = array(
'PartNumber' => ($i + 1),
'ETag' => $value
);
}
$completer = $this->completeMultipartUpload($bucket, $object, $options['upload_id'], $uploadParts);
$options['url'] = $completer['info']['url'];
}
return $options;
}
分片上传成功后记得删除本地的分片文件,文件合并后会返回 url ,可以存到另一个已上传文件表中,把日志记录表的该文件记录删除。
本人萌新码农一枚,有错误或是意见建议的话,欢迎探讨。
|