PHP搭建大文件切割分块上传功能示例
转载:https://www.jb51.net/article/101931.htm
背景
在网站开发中,文件上传是很常见的一个功能。相信很多人都会遇到这种情况,想传一个文件上去,然后网页提示“该文件过大”。因为一般情况下,我们都需要对上传的文件大小做限制,防止出现意外的情况。
但是在有些业务场景中,大文件上传又是必须的,比如邮箱附件,或者内部OA等等。
问题
服务端为什么不能直接传大文件?跟php.ini里面的几个配置有关
upload_max_filesize = 2M
//PHP最大能接受的文件大小
post_max_size = 8M
//PHP能收到的最大POST值'
memory_limit = 128M
//内存上限
max_execution_time = 30
//最大执行时间
当然不能简单粗暴的把上面几个值调大,否则服务器内存资源吃光是迟早的问题。
解决思路
好在HTML5开放了新的FILE API,也可以直接操作二进制对象,我们可以直接在浏览器端实现文件切割,按照以前的做法就得用Flash的方案,实现起来会麻烦很多。
JS思路
1.监听上传按钮的onchange事件
2.获取文件的FILE对象
3.把文件的FILE对象进行切割,并且附加到FORMDATA对象中
4.把FORMDATA对象通过AJAX发送到服务器
5.重复3、4步骤,直到文件发送完。
PHP思路
1.建立上传文件夹
2.把文件从上传临时目录移动到上传文件夹
3.所有的文件块上传完成后,进行文件合成
4.删除文件夹
5.返回上传后的文件路径
DEMO代码
前端部分代码
<!doctype html>
<html lang=
"en"
>
<head>
<meta charset=
"UTF-8"
>
<meta name=
"viewport"
content=
"width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
>
<meta http-equiv=
"X-UA-Compatible"
content=
"ie=edge"
>
<title>Document</title>
<style>
#progress{
width: 300px;
height: 20px;
padding: 0px 0px 0px 5px; background-image: initial; background-position: initial; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial; border-left: 3px solid rgb(108, 226, 108); line-height: 20px; width: 640px; clear: both; outline: 0px !important; border-radius: 0px !important; border-top: 0px !important; border-right: 0px !important; border-bottom: 0px !important; border-image: initial !important; bottom: auto !important; float: none !important; height: auto !important; left: auto !important; overflow: visible !important; position: static !important; right: auto !important; top: auto !important; vertical-align: baseline !important; box-sizing: content-box !important; font-family: Consolas, "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; min-height: auto !important; color: gray !important;">#f7f7f7;
box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);
border-radius:4px;
background-image:linear-gradient(to bottom,
#f5f5f5,#f9f9f9);
}
#finish{
padding: 0px 0px 0px 5px; background-image: initial; background-position: initial; background-size: initial; background-repeat: initial; background-attachment: initial; background-origin: initial; background-clip: initial; border-left: 3px solid rgb(108, 226, 108); line-height: 20px; width: 640px; clear: both; outline: 0px !important; border-radius: 0px !important; border-top: 0px !important; border-right: 0px !important; border-bottom: 0px !important; border-image: initial !important; bottom: auto !important; float: none !important; height: auto !important; left: auto !important; overflow: visible !important; position: static !important; right: auto !important; top: auto !important; vertical-align: baseline !important; box-sizing: content-box !important; font-family: Consolas, "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; min-height: auto !important; color: gray !important;">#149bdf;
background-image:linear-gradient(45deg,rgba(255,255,255,0.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,0.15) 50%,rgba(255,255,255,0.15) 75%,transparent 75%,transparent);
background-size:40px 40px;
height: 100%;
}
form{
margin-top: 50px;
}
</style>
</head>
<body>
<div id=
"progress"
>
<div id=
"finish"
style=
"width: 0%;"
progress=
"0"
></div>
</div>
<form action=
"./upload.php"
>
<input type=
"file"
name=
"file"
id=
"file"
>
<input type=
"button"
value=
"停止"
id=
"stop"
>
</form>
<script>
var
fileForm = document.getElementById(
"file"
);
var
stopBtn = document.getElementById(
'stop'
);
var
upload =
new
Upload();
fileForm.onchange =
function
(){
upload.addFileAndSend(
this
);
}
stopBtn.onclick =
function
(){
this
.value =
"停止中"
;
upload.stop();
this
.value =
"已停止"
;
}
function
Upload(){
var
xhr =
new
XMLHttpRequest();
var
form_data =
new
FormData();
const LENGTH = 1024 * 1024;
var
start = 0;
var
end = start + LENGTH;
var
blob;
var
blob_num = 1;
var
is_stop = 0
//对外方法,传入文件对象
this
.addFileAndSend =
function
(that){
var
file = that.files[0];
blob = cutFile(file);
sendFile(blob,file);
blob_num += 1;
}
//停止文件上传
this
.stop =
function
(){
xhr.abort();
is_stop = 1;
}
//切割文件
function
cutFile(file){
var
file_blob = file.slice(start,end);
start = end;
end = start + LENGTH;
return
file_blob;
};
//发送文件
function
sendFile(blob,file){
var
total_blob_num = Math.ceil(file.size / LENGTH);
form_data.append(
'file'
,blob);
form_data.append(
'blob_num'
,blob_num);
form_data.append(
'total_blob_num'
,total_blob_num);
form_data.append(
'file_name'
,file.name);
xhr.open(
'POST'
,
'./upload.php'
,
false
);
xhr.onreadystatechange =
function
() {
var
progress;
var
progressObj = document.getElementById(
'finish'
);
if
(total_blob_num == 1){
progress =
'100%'
;
}
else
{
progress = Math.min(100,(blob_num/total_blob_num)* 100 ) +
'%'
;
}
progressObj.style.width = progress;
var
t = setTimeout(
function
(){
if
(start < file.size && is_stop === 0){
blob = cutFile(file);
sendFile(blob,file);
blob_num += 1;
}
else
{
setTimeout(t);
}
},1000);
}
xhr.send(form_data);
}
}
</script>
</body>
</html>
<?php
class
Upload{
private
$filepath
=
'./upload'
;
//上传目录
private
$tmpPath
;
//PHP文件临时目录
private
$blobNum
;
//第几个文件块
private
$totalBlobNum
;
//文件块总数
private
$fileName
;
//文件名
public
function
__construct(
$tmpPath
,
$blobNum
,
$totalBlobNum
,
$fileName
){
$this
->tmpPath =
$tmpPath
;
$this
->blobNum =
$blobNum
;
$this
->totalBlobNum =
$totalBlobNum
;
$this
->fileName =
$fileName
;
$this
->moveFile();
$this
->fileMerge();
}
//判断是否是最后一块,如果是则进行文件合成并且删除文件块
private
function
fileMerge(){
if
(
$this
->blobNum ==
$this
->totalBlobNum){
$blob
=
''
;
for
(
$i
=1;
$i
<=
$this
->totalBlobNum;
$i
++){
$blob
.=
file_get_contents
(
$this
->filepath.
'/'
.
$this
->fileName.
'__'
.
$i
);
}
file_put_contents
(
$this
->filepath.
'/'
.
$this
->fileName,
$blob
);
$this
->deleteFileBlob();
}
}
//删除文件块
private
function
deleteFileBlob(){
for
(
$i
=1;
$i
<=
$this
->totalBlobNum;
$i
++){
@unlink(
$this
->filepath.
'/'
.
$this
->fileName.
'__'
.
$i
);
}
}
//移动文件
private
function
moveFile(){
$this
->touchDir();
$filename
=
$this
->filepath.
'/'
.
$this
->fileName.
'__'
.
$this
->blobNum;
move_uploaded_file(
$this
->tmpPath,
$filename
);
}
//API返回数据
public
function
apiReturn(){
if
(
$this
->blobNum ==
$this
->totalBlobNum){
if
(
file_exists
(
$this
->filepath.
'/'
.
$this
->fileName)){
$data
[
'code'
] = 2;
$data
[
'msg'
] =
'success'
;
$data
[
'file_path'
] =
'http://'
.
$_SERVER
[
'HTTP_HOST'
].dirname(
$_SERVER
[
'DOCUMENT_URI'
]).
str_replace
(
'.'
,
''
,
$this
->filepath).
'/'
.
$this
->fileName;
}
}
else
{
if
(
file_exists
(
$this
->filepath.
'/'
.
$this
->fileName.
'__'
.
$this
->blobNum)){
$data
[
'code'
] = 1;
$data
[
'msg'
] =
'waiting for all'
;
$data
[
'file_path'
] =
''
;
}
}
header(
'Content-type: application/json'
);
echo
json_encode(
$data
);
}
//建立上传文件夹
private
function
touchDir(){
if
(!
file_exists
(
$this
->filepath)){
return
mkdir
(
$this
->filepath);
}
}
}
//实例化并获取系统变量传参
$upload
=
new
Upload(
$_FILES
[
'file'
][
'tmp_name'
],
$_POST
[
'blob_num'
],
$_POST
[
'total_blob_num'
],
$_POST
[
'file_name'
]);
//调用方法,返回结果
$upload
->apiReturn();
存在的问题
这只是一个简单的DEMO,有很多地方需要改进,比如上传的文件夹与临时文件放在一起,用户中途取消也没有发请求进行清理,容易造成文件冗余。JS采用的是同步模型,文件需要一块一块按顺序上传,会导致整个浏览器在上传的过程中出于堵塞的状态,按了按钮可能需要几秒钟才能反应过来,用户体验不好。真正需要产品化的时候就要综合考虑多种情况,当然作为一个示例,引导大家了解分块上传的思路还是不错的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。