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

PHPABC Html5 1,514 次浏览 , 没有评论

工作中,经常碰到大文件上传的需求,如上传大的用户包、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/

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据

Go