工作中,经常碰到大文件上传的需求,如上传大的用户包、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}"); } .... |
上面方案存在一个致命的问题,没发处理大文件的上传。主要受到来自下面几个方面的限制:
1.PHP的脚本时长的限制
2.长时间上传,网络中断的问题
3.PHP脚本执行时内存大小等的限制
4.apache服务器链接数限制
与之相关的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等设置,将 如果中间网络中断,上传文件还是失败。
并且上述方案还有下面几个问题:
1.不支持显示上传进度
2.不支持断点续传、如果网络中断后需要整体重传
我们希望能够找到一个在web上能够解决上面两个问题的方案。
HTML5之前的解决方案
一般有三种方法:
1.RIA技术(Flex Silverlight等),该方案需要浏览器支持Flash,最常用的是使用SWFupload实现。
2.插件技术(ActiveX,applet等),要装下载安装插件、比较麻烦。而且ActiveX插件不具有跨浏览器的特性,只能在IE浏览器上使用。
3.CS解决方案:开发单独的客户端和服务器程序,使用UDP/TCP长链接通讯上传。缺点:需要单独开发Client,发布更新困难、
html5的解决方案
相关对象和API
File – 独立文件;提供只读信息,例如名称、文件大小、mimetype 和对文件句柄的引用。
FileList – File 对象的类数组序列(考虑 或者从桌面拖动目录或文件)。
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; } <input type="file" id="files" name="file"/> <div id="progress_bar"> <div class="percent">0%</div> </div> //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); |
另:
大文件上传在后端还要考虑如何支持接入服务器集群部署,把数据传递给后端的文件处理服务器。
参考文献:
http://www.html5rocks.com/zh/tutorials/file/dndfiles/