工作中,经常碰到大文件上传的需求,如上传大的用户包、CDK列表等。常规的解决方案是采用form表单+ 方式提交给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>


 <  name=\"hidden_target\" id=\"hidden_target\" src=\"about:blank\" style=\"display:none;\"></ >

php代码:

if (is_uploaded_file($_FILES[\"sCDKFile\"][\"tmp_name\"])) {
	switch ($_FILES[\"sCDKFile\"][\"error\"]) {
		case UPLOAD_ERR_OK:
			break;

		case UPLOAD_ERR_NO_FILE:
			$this->Output (\'No file sent.\');
		case UPLOAD_ERR_INI_SIZE:
		case UPLOAD_ERR_FORM_SIZE:
			$this->Output (\'Exceeded filesize limit.\');
		default:
			$this->Output (\'Unknown errors. Code:\' . $_FILES[\"sCDKFile\"][\"error\"]);
	}

	 //上传文件的类型
	$stype = $_FILES[\"sCDKFile\"][\"type\"];
	//如果文件符合要求并且上传过程中没有错误
	if ($stype != \"text/plain\" && $stype != \"application/csv\") {
		$this->Output (\"请选择上传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, 等),要装下载安装插件、比较麻烦。而且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;
        }


       <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.  = 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.  = function (e) {
            console.debug(e.target.error.message);
            //alert(\'File read cancelled\');
        };
        reader. start = function (e) {
            document.getElementById(\'progress_bar\').className = \'loading\';
        };


        reader.  = 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; 64,\"){
    $data = substr($data, 37);
}

$data =  64_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/

收藏 打印