用HTML5 API解决大文件上传问题

现在的困难

工作中,经常碰到大文件上传的需求,如上传大的用户包、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的脚本时长的限制
  • 长时间上传,网络中断的问题
  • PHP脚本执行时内存大小等的限制
  • 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 对象的类数组序列(考虑 <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);

One more thing

大文件上传在后端还要考虑如何支持接入服务器集群部署,把数据传递给后端的文件处理服务器。

结束语:

感谢html5!积极拥抱新技术,这是快速提高生产力的最有效手段!

参考文献:
http://www.html5rocks.com/zh/tutorials/file/dndfiles/

你可能感兴趣的:(web)