工作中,经常碰到大文件上传的需求,如上传大的用户包、CDK列表等。常规的解决方案是采用form表单+iframe方式提交给php处理,如下面的代码:
//html
<form enctype="multipart/form-data" target="hidden_target"
action="CRobFloor.php?a=Upload" method="POST"
id="frmImportCDK">
<label class="col-lg-3">请选择CDK文件label>
<input type="file" name="sCDKFile" require="true" datatype="require" msg="请选择文件" id="tFileUpload"/>
<button type="submit" id="btnUpload">上传button>
form>
<iframe name="hidden_target" id="hidden_target" src="about:blank" style="display:none;">iframe>
//php
if (is_uploaded_file($_FILES["sCDKFile"]["tmp_name"])) {
switch ($_FILES["sCDKFile"]["error"]) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
$this->OutputScript('No file sent.');
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$this->OutputScript('Exceeded filesize limit.');
default:
$this->OutputScript('Unknown errors. Code:' . $_FILES["sCDKFile"]["error"]);
}
//上传文件的类型
$stype = $_FILES["sCDKFile"]["type"];
//如果文件符合要求并且上传过程中没有错误
if ($stype != "text/plain" && $stype != "application/csv") {
$this->OutputScript("请选择上传txt,csv格式的文件,不支持格式{$stype}");
}
....
上面方案存在一个致命的问题,没发处理大文件的上传。主要受到来自下面几个方面的限制:
与之相关的php配置项如下:
;;;;;;;;;;;;;;;;
; File Uploads ;
;;;;;;;;;;;;;;;;
file_uploads = On
upload_max_filesize = 8m //允许上传文件大小的最大值
max_file_uploads = 20 //单请求最多允许上传文件数量
post_max_size = 8M //post数据大小限制
;;;;;;;;;;;;;;;;;;
; Resource Limits ;
;;;;;;;;;;;;;;;;;;;
max_execution_time = 30 ;每个PHP页面运行的最大时间值(秒),默认30秒
max_input_time = 60 ;每个PHP页面接收数据所需的最大时间,默认60秒
memory_limit = 128m ;每个PHP页面所吃掉的最大内存,默认8M
从上面的配置看到,如果调大upload_max_filesize = 128m,设上传网速为 500KB,则30s只够上传15M以内大小的文件,时间长了脚本将中断执行,如果继续调整max_execution_time等设置,将 如果中间网络中断,上传文件还是失败。
并且上述方案还有下面几个问题:
我们希望能够找到一个在web上能够解决上面两个问题的方案。
一般有三种方法:
相关对象和API
File - 独立文件;提供只读信息,例如名称、文件大小、mimetype 和对文件句柄的引用。
FileList - File 对象的类数组序列(考虑 <input type="file" multiple> 或者从桌面拖动目录或文件)。
Blob - 可将文件分割为字节范围。
FileReader:文件读写对象,包括四个异步读取文件的选项:
FileReader.readAsBinaryString(Blob|File) - result 属性将包含二进制字符串形式的 file/blob 数据。每个字节均由一个 [0..255] 范围内的整数表示。
FileReader.readAsText(Blob|File, opt_encoding) - result 属性将包含文本字符串形式的 file/blob 数据。该字符串在默认情况下采用“UTF-8”编码。使用可选编码参数可指定其他格式。
FileReader.readAsDataURL(Blob|File) - result 属性将包含编码为数据网址的 file/blob 数据。
FileReader.readAsArrayBuffer(Blob|File) - result 属性将包含 ArrayBuffer 对象形式的 file/blob 数据。
HTML5文件处理API能够支持文件拖拽、上传进度显示、支持文件分块读取等特性。有了文件分块读取的特性,就可以实现将文件分块上传,然后在服务器段合并文件。如下面的demo:
#progress_bar {
margin: 10px 0;
padding: 3px;
border: 1px solid #000;
font-size: 14px;
clear: both;
opacity: 0;
-moz-transition: opacity 1s linear;
-o-transition: opacity 1s linear;
-webkit-transition: opacity 1s linear;
}
#progress_bar.loading {
opacity: 1.0;
}
#progress_bar .percent {
background-color: #99ccff;
height: auto;
width: 0;
}
"file" id="files" name="file"/>
"progress_bar">
"percent">0%
//todo流程:
//0. 读取文件长度
//1. 初始化显示信息
//1. 读取数据
//2. 发送给后台
//3. 接收返回值、更新收到的数据
var uploadData = function (data, size, beg, end, callback) {
$.post("testUpload.php",
{
"size": size,
"data": data,
"beg": beg,
"end": end
}).done(function (result) {
if (result.indexOf("Success") != -1) {
callback();
} else {
console.log("Error:\n" + data);
alert("文件块上传失败,请重新上传文件!");
}
});
};
var handleFileSelect = function (evt) {
var reader;
var progress = document.querySelector('.percent');
progress.style.width = '0%';
progress.textContent = '0%';
var files = document.getElementById('files').files;
if (!files.length) {
alert('请选择文件');
return;
}
var file = files[0];
var length = 1024 * 1024; //1M
var hadRead = 0;
var start = 0;
var stop = 0;
readBob = function (start, stop) {
console.log("Read:[" + start + ':' + stop + ']');
if (file.webkitSlice) {
var blob = file.webkitSlice(start, stop);
} else if (file.mozSlice) {
var blob = file.mozSlice(start, stop);
} else {
var blob = file.slice(start, stop);
}
reader.readAsDataURL(blob);
}
reader = new FileReader();
reader.onerror = function (evt) {
console.debug(evt.target.error.message);
switch (evt.target.error.code) {
case evt.target.error.NOT_FOUND_ERR:
alert('File Not Found!');
break;
case evt.target.error.NOT_READABLE_ERR:
alert('File is not readable');
break;
case evt.target.error.ABORT_ERR:
console.debug("errorHandler ABORT_ERROR");
break; // noop
default:
alert('An error occurred reading this file.');
}
;
};
reader.onabort = function (e) {
console.debug(e.target.error.message);
//alert('File read cancelled');
};
reader.onloadstart = function (e) {
document.getElementById('progress_bar').className = 'loading';
};
reader.onload = function (e) {
if (reader.readyState == FileReader.DONE) { // DONE == 2
var callback = function () {
hadRead = stop;
progress.style.width = Math.round(hadRead / file.size * 100) + '%';
progress.textContent = Math.round(hadRead / file.size * 100) + '%';
if (hadRead >= file.size) {
return;
}
stop = (hadRead + length) > file.size ? (file.size) : (hadRead + length);
start = hadRead;
readBob(start, stop);
}
uploadData(reader.result, file.size, start, stop, callback);
//setTimeout(callback, 1000);
}
}
stop = (hadRead + length) > file.size ? (file.size) : (hadRead + length);
readBob(start, stop);
}
// Check for the various File API support.
if (window.File && window.FileReader && window.FileList && window.Blob) {
document.getElementById('files').addEventListener('change', handleFileSelect, false);
} else {
alert('您的浏览器不支持HTML5 API,请使用最新版本的chrome或者firefox浏览器');
}
//php
//TODO:
//1. 合并同一个文件、
//2. 判断是否文件结束
//3. 多用户并发情况下,不能互相干扰
session_start();
$data = $_POST["data"];
if(substr($data, 0, 37) == "data:application/octet-stream;base64,"){
$data = substr($data, 37);
}
$data = base64_decode($data);
$size = $_POST["size"];
$end = $_POST["end"];
$beg = $_POST["beg"];
if($beg == 0){
$filename = tempnam("/tmp", "FOO");
$_SESSION["filename"] = $filename;
}else{
$filename = $_SESSION["filename"];
}
// Let's make sure the file exists and is writable first.
if (!$handle = fopen($filename, 'a')) {
echo "Cannot open file ($filename)";
exit;
}
// Write $somecontent to our opened file.
if (fwrite($handle, $data) === FALSE) {
echo "Cannot write to file ($filename)";
exit;
}
fclose($handle);
if($size == $end){
unset($_SESSION["filename"]);
chmod($filename, 0755);
$newName = "Date".date("YmdHis", time()).".jpg";
rename($filename, $newName);
echo $newName;
}
echo "Success";
exit(0);
大文件上传在后端还要考虑如何支持接入服务器集群部署,把数据传递给后端的文件处理服务器。
感谢html5!积极拥抱新技术,这是快速提高生产力的最有效手段!
参考文献:
http://www.html5rocks.com/zh/tutorials/file/dndfiles/