ThinkPHP6+JS实现大文件分片上传,切片上传
发表于:2023-02-23 23:00:59浏览:1776次
在日常的项目中,对于很大的文件比如几百M的音频、视频、软件上传,如果直接上传到服务器,经常处理不了。可以在客户端先把大文件切割成小文件一个一个上传,然后服务器端再组合成一个大文件,同时支持上传取消删除已上传的临时文件。
<div class="layui-input-block">
<button type="button" class="layui-btn" id="fileUpload">上传文件</button>
</div>
$('#fileUpload').on('click',function(){
uploadChunk();
})
function uploadChunk(){
layer.open({
'title':'上传文件',
'type':1,
'area': ['600px', '320px'],
'content':`<div class="layui-form p-3">
<div class="layui-form-item">
<label class="layui-form-label">来源:</label>
<div class="layui-input-block">
<input type="radio" name="uploadtype" lay-filter="type" value="1" title="本地上传" checked>
<input type="radio" name="uploadtype" lay-filter="type" value="2" title="网络文件">
</div>
</div>
<div id="uploadType1">
<div class="layui-form-item">
<label class="layui-form-label">文件:</label>
<div class="layui-input-block">
<span class="gougu-upload-files">${typeTps[type]}</span><button type="button" class="layui-btn layui-btn-normal" onclick="document.querySelector('#chunkFile').click()">选择文件</button>
<input type="file" id="chunkFile" class="layui-upload-file" accept="${typeExt[type]}">
</div>
</div>
<div class="layui-form-item">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<span class="gougu-upload-tips">只能上传 ${typeTps[type]} 文件</span>
</div>
</div>
<div class="layui-progress upload-progress" lay-showpercent="yes" lay-filter="upload-progress" style="margin-left:100px; width:420px; display:none;">
<div class="layui-progress-bar layui-bg-blue" lay-percent=""><span class="layui-progress-text"></span></div>
<div style="padding-top:15px; font-size:12px" id="gougu-upload-choosed"></div>
<div style="padding-top:12px; display:none;">
<button type="button" class="layui-btn layui-btn-sm layui-btn-danger" id="chunkDel">取消上传</button>
</div>
</div>
</div>
<div id="uploadType2" style="display:none; width:500px;">
<div class="layui-form-item">
<label class="layui-form-label">URL地址:</label>
<div class="layui-input-block">
<input type="text" name="web_url" placeholder="" autocomplete="off" class="layui-input">
</div>
</div>
<div class="layui-form-item layui-form-item-sm">
<label class="layui-form-label"></label>
<div class="layui-input-block">
<span class="layui-btn" id="saveAjax">确定提交</span>
</div>
</div>
</div>
</div>`,
success: function(layero, index){
form.render();
form.on('radio(type)', function(data){
if(data.value==1){
$('#uploadType1').show();
$('#uploadType2').hide();
}
else{
$('#uploadType1').hide();
$('#uploadType2').show();
}
});
// 监听文件上传
$('#chunkFile').change(function (e) {
var uploadFile = $(this)[0].files[0];
console.log(uploadFile,e);
var fileReader = new FileReader();
var fileId = getRandom();
$('#chunkDel').on('click',function(){
layer.confirm('确定要取消上传吗?', {icon: 3, title:'提示'}, function(idx){
loadding = false;
setTimeout(() => {
clearChunk()
},500);
layer.close(idx);
});
});
//读取文件名称
var fileName = uploadFile.name;
//读取文件后缀
var fileExtension = fileName.substring(fileName.lastIndexOf('.') + 1);
//读取文件大小
var fileSize = uploadFile.size;
// 定义每个区块的大小,以字节为单位
var chunk = 10 * 1024 * 1024;//每片10M
// 初始化记录的进度
var loaded = 0,loadding=true;
readPartFile(loaded,chunk);
// 监听加载完毕的区块
fileReader.onload = function (event) {
// 更新进度
loaded += chunk;
var targetResult = event.target.result;
// 转换成Blob对象
var targetBlob = new Blob([targetResult]);
console.log('event',event,'mime',targetBlob);
var progress = (loaded*100/fileSize).toFixed(0);
if(progress>=100){
progress = 99;
}
$('#gougu-upload-choosed').html('正在上传:'+fileName);
$('.upload-progress').show();
element.progress('upload-progress', progress + '%');
//$('#load').html(progress+"%");
var isEnd = loaded >= fileSize ? 1 : 0;
//每1秒上传一次,防止请求频率过快,时间可自定义
setTimeout(() => {
pieceUpload(targetBlob,isEnd,fileId,fileName,function (e) {
console.log(e);
if (isEnd) {
loadding=false;
layer.msg(e.msg);
$('[name="file_id"]').val(e.data.id);
$('[name="url"]').val(e.data.filepath);
layer.close(index);
return e;
}
//当前区块上传完毕后再上传下一个区块
if(loadding == true){
readPartFile(loaded,chunk);
}
})
},500)
}
// 将文件分成指定大小的区块,start是起始位置
function readPartFile(start) {
var piece = uploadFile.slice(start,start + chunk);
fileReader.readAsArrayBuffer(piece);
}
// 分块上传 file:上传的区块 isEnd:是否已整体上传结束 fileId:源文件的唯一标识 filename:文件名 callback:上传之后的回调
function pieceUpload(file,isEnd,fileId,filename,callback) {
try {
var formData = new FormData();
formData.append('file',file,'file');
formData.append('is_end',isEnd);
formData.append('file_id',fileId);
formData.append('file_name',filename);
formData.append('file_extension',fileExtension);
formData.append('file_size',fileSize);
//向服务端表明该请求是一个分块上传
formData.append('type','chunk');
console.log(formData);
$.ajax({
//分块上传的接口url
url: '/api/index/chunkUpload',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: callback,
error: function (err) {
console.log(err)
}
})
} catch (e) {
console.log(e)
}
}
function clearChunk(){
//删除分片上传的临时文件
$.ajax({
url: '/api/index/clearChunk',
type: 'POST',
data: {'file_id':fileId},
success: function(e){
layer.msg(e.msg);
layer.close(index);
},
error: function (err) {
console.log(err);
}
})
}
//生成32位随机数
function getRandom(){
var chars = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
var str="";
for(var i=0;i<32;i++){
var id = parseInt(Math.random()*35);
str+=chars[id];
}
return str;
}
})
$('#saveAjax').on('click',function(){
let url=$('[name="web_url"]').val();
if(url == ''){
layer.msg('请输入网络URL');
return false;
}
$('[name="url"]').val(url);
layer.close(index);
})
}
});
}
<?php
declare (strict_types = 1);
namespace app\api\controller;
use app\api\BaseController;
use think\facade\Db;
class Index extends BaseController
{
//执行分片上传的控制器方法
public function chunkUpload() {
if ($this->request->isPost()) {
//执行分片上传流程
$data = $this->request->post();
//判断是否是分片上传
if ($data['type'] === 'chunk') {
$file = request()->file('file');
//获取对应的上传配置
$fs = \think\facade\Filesystem::disk('public');
$ext = $file->extension();
$chunkPath = $data['file_id'].'/'.$file->md5().($ext ? '.'.$ext : '');
//存储分片文件到指定路径
$savename = $fs->putFileAs( 'chunk', $file,$chunkPath);
if (!$savename) {
return json([
'code' => 1,
'msg' => '上传失败',
'data' => [],
]);
}
if (!$data['is_end']) {
$filepath = '';
} else {
//合并块文件
$fileUrl = '';
$chunkSaveDir = \think\facade\Filesystem::getDiskConfig('public');
$smallChunkDir = $chunkSaveDir['root'].'/chunk/'.$data['file_id'];
//获取已存储的属于源文件的所有分块文件 进行合并
if ($handle = opendir($smallChunkDir)) {
$chunkList = [];
$modifyTime = [];
while (false !== ($file = readdir($handle))) {
if ($file != "." && $file != "..") {
$temp['path'] = $smallChunkDir.'/'.$file;
$temp['modify'] = filemtime($smallChunkDir.'/'.$file);
$chunkList[] = $temp;
$modifyTime[] = $temp['modify'];
}
}
//对分块文件进行排序
array_multisort($modifyTime,SORT_ASC,$chunkList);
$saveDir = \think\facade\Filesystem::getDiskConfig('public');
$saveName = md5($data['file_id'].$data['file_name']).'.'.$data['file_extension'];
$newPath = $saveDir['root'].'/'.date('Ym').'/'.$saveName;
if (!file_exists($saveDir['root'].'/'.date('Ym'))) {
mkdir($saveDir['root'].'/'.date('Ym'),0777,true);
}
$newFileHandle = fopen($newPath,'a+b');
foreach ($chunkList as $item) {
fwrite($newFileHandle,file_get_contents($item['path']));
unlink($item['path']);
}
rmdir($smallChunkDir);
//将合并后的文件存储到指定路径
$fileUrl = $saveDir['url'].'/'.date('Ym').'/'.$saveName;
fclose($newFileHandle);
closedir($handle);
} else {
return json([
'code' => 1,
'msg' => '目录:'.$chunkSaveDir['root'].'/chunk/'.$data['file_id'].'不存在',
'data' => [],
]);
}
$filepath = $fileUrl;
}
$res=[];
//合并流程结束
if ($filepath!='') {
$fileinfo = [];
$fileinfo['filepath'] = $filepath;
$fileinfo['name'] = $data['file_name'];
$fileinfo['fileext'] = $data['file_extension'];
$fileinfo['filesize'] = $data['file_size'];
$fileinfo['filename'] = date('Ym').'/'.$saveName;
$fileinfo['sha1'] = $data['file_id'];
$fileinfo['md5'] = $data['file_id'];
$fileinfo['module'] = \think\facade\App::initialize()->http->getName();
$fileinfo['action'] = app('request')->action();
$fileinfo['uploadip'] = app('request')->ip();
$fileinfo['create_time'] = time();
$fileinfo['user_id'] = get_login_admin('id') ? get_login_admin('id') : 0;
if ($fileinfo['module'] = 'admin') {
//通过后台上传的文件直接审核通过
$fileinfo['status'] = 1;
$fileinfo['admin_id'] = $fileinfo['user_id'];
$fileinfo['audit_time'] = time();
}
$fileinfo['use'] = 'big';
$res['id'] = Db::name('file')->insertGetId($fileinfo);
$res['filepath'] = $fileinfo['filepath'];
$res['name'] = $fileinfo['name'];
$res['filename'] = $fileinfo['filename'];
$res['filesize'] = $fileinfo['filesize'];
$res['fileext'] = $fileinfo['fileext'];
add_log('upload', $fileinfo['user_id'], $fileinfo);
}
return to_assign(0, '上传成功', $res);
}
}
}
//取消上传,删除临时文件
public function clearChunk() {
if ($this->request->isPost()) {
$param = get_params();
$saveDir = \think\facade\Filesystem::getDiskConfig('public');
$smallChunkDir = $saveDir['root'].'/chunk/'.$param['file_id'];
if(!is_dir($smallChunkDir)){
return to_assign(0, '上传的临时文件已删除');
}
//获取已存储的属于源文件的所有分块文件
if ($handle = opendir($smallChunkDir)) {
while (false !== ($file = readdir($handle))) {
if ($file != "." && $file != "..") {
$temp['path'] = $smallChunkDir.'/'.$file;
unlink($temp['path']);
}
}
rmdir($smallChunkDir);
closedir($handle);
return to_assign(0, '已取消上传');
}
}
}
PS: Nginx上传大文件超时的解决办法。
用nginx作代理服务器,上传大文件时(本人测试上传50m的文件),提示上传超时或文件过大。原因是nginx对上传文件大小有限制,而且默认是1M。另外,若上传文件很大,还要适当调整上传超时时间。
解决方法是在nginx的配置文件下,加上以下配置:
client_max_body_size 50m; //文件大小限制,默认1m
client_header_timeout 1m;
client_body_timeout 1m;
proxy_connect_timeout 60s;
proxy_read_timeout 1m;
proxy_send_timeout 1m;
PS:PHP上传大文件超过40秒服务器500的解决办法
找当前PHP版本的配置文件php.ini修改以下文件上传限制参数:
max_execution_time = 600
max_input_time = 600
memory_limit = 1024M
post_max_size = 1024M
upload_max_filesize = 1024M