基于AJAX / PHP的上传,包含大文件的进度条

时间:2012-04-20 19:23:04

标签: php ajax file upload progress

我一直在尝试创建一个非Flash上​​传面板,它还会显示一个进度条。 在我们的服务器上,我们有PHP 5.3(目前无法升级到5.4,因此无法使用新的上传进度功能=> http://php.net/manual/en/session.upload-progress.php)。 我们不能使用基于闪存的解决方案,扩展或类似的。

因此我尝试将XMLHttpRequest与AJAX结合使用。 这里的问题是我只取得了部分成功。

我设法上传并在服务器上保存了大约380 MB的文件,但是,当尝试使用像4 GB这样的大文件时,它将不会保存在服务器上(如果我在一台服务器上查看Firebug)它会说“POST aborted”。)

另一个奇怪的事情是,使用相同的文件,xhr.upload.load以相同的xhr.upload.total维度开始,并从那里开始计数。

有谁知道如何解决这个问题或有替代解决方案?

客户端代码是:

<script type="application/javascript" src="jquery.js"></script>

<script type="application/javascript">

function uploadToServer()
{
    fileField = document.getElementById("uploadedFile");
    var fileToUpload = fileField.files[0]; 

    var xhr = new XMLHttpRequest();
    var uploadStatus = xhr.upload;

    uploadStatus.addEventListener("progress", function (ev) {
            if (ev.lengthComputable) {
                $("#uploadPercentage").html((ev.loaded / ev.total) * 100 + "%");
            }
        }, false);

    uploadStatus.addEventListener("error", function (ev) {$("#error").html(ev)}, false);
    uploadStatus.addEventListener("load", function (ev) {$("#error").html("APPOSTO!")}, false);

    xhr.open(
            "POST",
            "serverUpload.php",
            true
            );
        xhr.setRequestHeader("Cache-Control", "no-cache");
        xhr.setRequestHeader("Content-Type", "multipart/form-data");
        xhr.setRequestHeader("X-File-Name", fileToUpload.fileName);
        xhr.setRequestHeader("X-File-Size", fileToUpload.fileSize);
        xhr.setRequestHeader("X-File-Type", fileToUpload.type);
        //xhr.setRequestHeader("Content-Type", "application/octet-stream");
        xhr.send(fileToUpload);
}



$(function(){

    $("#uploadButton").click(uploadToServer);

});


</script>

HTML部分:

<form action="" name="uploadForm" method="post" enctype="multipart/form-data">

  <input id="uploadedFile" name="fileField" type="file" multiple />

<input id="uploadButton" type="button" value="Upload!">

</form>

<div id="uploadPercentage"></div>
<div id="error"></div>

服务器端代码:

<?php

$path = "./";
$filename = $_SERVER['HTTP_X_FILE_NAME'];
$filesize = $_SERVER['CONTENT_LENGTH'];


$file = "log.txt";
$fo= fopen($file, "w");
fwrite($fo, $path . PHP_EOL);
fwrite($fo, $filename . PHP_EOL);
fwrite($fo, $filesize . PHP_EOL);
fwrite($fo, $path . $filename . PHP_EOL);

file_put_contents($path . $filename, 
file_get_contents('php://input')
);

?>

6 个答案:

答案 0 :(得分:3)

与Web无法通过PHP更改的Web服务器相关联。例如,它们是IIS中默认的最大发布请求大小30MB ...还有一个您可能遇到的最大超时。与大小无关,但是您的帖子请求需要多长时间...即,文件提交需要多长时间。这两种设置都可以由IIS或Apache约束。

答案 1 :(得分:3)

其他人已经指出,您将在正确配置的任何生产PHP服务器上遇到限制。内存,帖子和文件最大值开始。此外,httpd服务通常也会限制这些。

上传的答案如此之大,就是将文件剪切成块,将每个块发送到不同的put或post(取决于浏览器)。

已经存在一个能够上传块文件的库,所以我将以它为例。为了支持分块上传,上传处理程序使用Content-Range标头,该标头由插件为每个块传输。

UploadHandler类中的handle_file_upload函数是如何使用PHP处理服务器端的分块文件上传的一个很好的例子。 - https://github.com/blueimp/jQuery-File-Upload/blob/master/server/php/UploadHandler.php

function handle_file_upload($uploaded_file, $name, $size, $type, $error,
        $index = null, $content_range = null)

该函数接受参数$content_range = null,该参数在HTTP标头中传递给服务器,并从$_SERVER['HTTP_CONTENT_RANGE'];

中检索

稍后我们需要找出是否要将文件上传附加到已经存在的文件中,以便我们设置变量。如果HTTP请求中报告的文件大小大于服务器上的实际文件大小,则$content_range变量不为NULL且文件存在,我们需要将此上载附加到现有文件。

$append_file = $content_range && is_file($file_path) &&
            $file->size > $this->get_file_size($file_path);

大!现在怎么样?

所以现在我们需要知道我们如何接收数据。较旧版本的Firefox无法使用multipart / formdata(POST)进行分块文件上传。对于客户端和服务器端,需要以不同方式处理这些请求。

        if ($uploaded_file && is_uploaded_file($uploaded_file)) {
            // multipart/formdata uploads (POST method uploads)
            if ($append_file) {
            // append to the existing file
                file_put_contents(
                    $file_path,
                    fopen($uploaded_file, 'r'),
                    FILE_APPEND
                );
            } else {
            // this is a new chunked upload OR a completed single part upload,
            // so move the file from the temp directory to the uploads directory.
                move_uploaded_file($uploaded_file, $file_path);
            }
        }

根据文档:只有支持XHR文件上传的浏览器和Blob API支持分块文件上传,Blob API包括Google Chrome和Mozilla Firefox 4+ - https://github.com/blueimp/jQuery-File-Upload/wiki/Chunked-file-uploads

对于在Mozilla Firefox 4-6(Firefox 7之前支持XHR上载的Firefox版本)上工作的分块上传,多部分选项也必须设置为false。以下是在服务器端处理这些情况的代码。

        else {
            // Non-multipart uploads (PUT method support)
            file_put_contents(
                $file_path,
                fopen('php://input', 'r'),
                $append_file ? FILE_APPEND : 0
            );
        }

最后,我们可以验证下载是否完整,或者放弃已取消的上传。

        $file_size = $this->get_file_size($file_path, $append_file);
        if ($file_size === $file->size) {
            $file->url = $this->get_download_url($file->name);
            if ($this->is_valid_image_file($file_path)) {
                $this->handle_image_file($file_path, $file);
            }
        } else {
            $file->size = $file_size;
            if (!$content_range && $this->options['discard_aborted_uploads']) {
                unlink($file_path);
                $file->error = $this->get_error_message('abort');
            }
        }

在客户端,您需要跟踪块。在每件作品发布后,我们发送下一部分,直到没有剩下的块。示例库是jQuery的插件,它使它变得非常简单。使用像您这样的裸XHR对象需要更多代码。它可能看起来像这样:

var chunksize = 1000000 // 1MB
var chunks = math.ceil(chunksize / fileToUpload.fileSize);

function uploadChunk(fileToUpload, chunk = 0) {
     var xhr = new XMLHttpRequest();
     var uploadStatus = xhr.upload;

     uploadStatus.addEventListener("progress", function (ev) {
            if (ev.lengthComputable) {
                $("#uploadPercentage").html((ev.loaded / ev.total) * 100 + "%");
            }
        }, false);

     uploadStatus.addEventListener("error", function (ev) {$("#error").html(ev)}, false);
     uploadStatus.addEventListener("load", function (ev) {$("#error").html("APPOSTO!")}, false);

     var start = chunksize*chunk;
     var end = start+(chunksize-1)
     if (end >= fileToUpload.fileSize) {
            end = fileToUpload.fileSize-1;
     }

     xhr.open(
            "POST",
            "serverUpload.php",
            true
     );
     xhr.setRequestHeader("Cache-Control", "no-cache");
     xhr.setRequestHeader("Content-Type", "multipart/form-data");
     xhr.setRequestHeader("X-File-Name", fileToUpload.fileName);
     xhr.setRequestHeader("X-File-Size", fileToUpload.fileSize);
     xhr.setRequestHeader("X-File-Type", fileToUpload.type);
     xhr.setRequestHeader("Content-Range", start+"-"+end+"/"+fileToUpload.fileSize);
     xhr.send(fileToUpload);
}

for(c = 0; c < chunks; c++) {
     uploadChunk(fileToUpload, c);
}

循环遍历块,依次上传每个块区域。请注意,Content-Range标头值的格式为 start-end / size 。范围从0开始,因此“end”只能是“size”的最大值1。您可以使用范围“start - ”来指示范围从“start”扩展到文件的末尾。

修改

只是认为这样可以在服务器上实现进度条,否则单个文件上传是不可能的。由于您知道每个块的大小以及每个请求的状态,因此每次运行循环时都可以相应地更新状态栏。

另外值得注意的是某些浏览器的限制。 Chrome和Firefox应该能够处理4GB文件,但低于9的IE版本有一个错误,无法处理大于2GB的文件。

答案 2 :(得分:1)

您可以将您的代码与本教程进行比较。本教程可以上传任何大小的文件。它与您的代码非常相似。 http://www.youtube.com/watch?v=pTfVK73CUk8

答案 3 :(得分:0)

我正在写关于xhr.upload.loaded的奇怪行为,它以大量数字开头......

我有类似的问题,我找不到原因。 可能有帮助的唯一线索是依赖于ISP,问题有时会消失! 例如,当我在家测试它工作正常,我没有看到这种奇怪的行为,但从工作互联网,问题仍然存在。

答案 4 :(得分:0)

file_get_contents()获取文件的内容,并使用内部指针将其放入RAM中的BUFFER中。 如果你没有足够的ram,或者有32位版本的apache / php,那么在尝试分配太多内存时它可能会崩溃。

你可能想尝试这样的事情:

$upload = fopen("php://input", "r");
while (!feof($upload)) {
    file_put_contents($path . $filename, fread($upload, 4096), FILE_APPEND);
}
fclose($upload);

干杯

答案 5 :(得分:0)

我尝试使用ajax上传4GB的视频文件。这是成功的。 这是我的代码。

HTML ::

<form enctype="multipart/form-data" method="post">
<input type="file" id="video_file" name="video_file" accept=".mp4, .avi, .mkv">
<input type="submit" class="btn btn-success" id="video-upload-btn" name="video_upload_btn" value="Upload">
<div class="video-bar">
    <span class="video-bar-fill" id="video-bar-fill-id"><span class="video-bar-fill-text" id="video-bar-fill-text-id"></span></span>
</div>
</form>

CSS ::

.video-bar{
    width: 100%;
    background: #eee;
    padding: 3px;
    margin-bottom: 10px;
    box-shadow: inset 0 1px 3px rgba(0,0,0,0.2);
    border-radius: 3px;
    box-sizing: border-box;
}

.video-bar-fill{
    height: 20px;
    display: block;
    background: cornflowerblue;
    width: 0;
    border-radius: 3px;
    transition: width 0.8s ease;
}
.video-bar-fill-text{
    color: #fff;
    padding: 3px;
}

Ajax ::

<script type="text/javascript">
    var app = app || {};

    (function(video_op){
        "use strict";

        var video_ajax, video_getFormData, video_setProgress;

        video_ajax = function(data){

            var xmlhttp = new XMLHttpRequest(), uploaded;

            xmlhttp.addEventListener('readystatechange', function(){
                if(this.readyState==4){
                    if(this.status==200){
                        uploaded = JSON.parse(this.response);
                        console.log(uploaded);

                        if(typeof video_op.options.finished==='function'){
                            video_op.options.finished(uploaded);
                        }
                    } else {
                        if(typeof video_op.options.error === 'function'){
                            video_op.options.error();
                        }
                    }
                }
            });

            xmlhttp.upload.addEventListener("progress", function(event){
                var percent;
                if(event.lengthComputable===true){
                    percent = Math.round((event.loaded / event.total) * 100);
                    video_setProgress(percent);
                }

            });

            if(video_op.options.videoProgressBar!==undefined){
                video_op.options.videoProgressBar.style.width=0;
            }
            if(video_op.options.videoProgressText!==undefined){
                video_op.options.videoProgressText.innerText=0;
            }

            xmlhttp.open("post", video_op.options.videoProcessor);
            xmlhttp.send(data);

        };

        video_getFormData = function(source1){
            var data = new FormData(), i;

            for(i=0;i<source1.length; i++){
                data.append('video_file', source1[i]);
            }

            data.append("ajax", true);

            return data;

        };

        video_setProgress = function(value){
            if(video_op.options.videoProgressBar!==undefined){
                video_op.options.videoProgressBar.style.width = value? value+"%":0;
            }
            if(video_op.options.videoProgressText!==undefined){
                video_op.options.videoProgressText.innerText=value?value+"%":0;
            }
        };

        video_op.videouploader = function(options){
            video_op.options = options;

            if(video_op.options.videoFiles !== undefined){
                var videoFormDataValue = video_getFormData(video_op.options.videoFiles.files);

                video_ajax(videoFormDataValue);
            }
        }

    }(app));

    document.getElementById("video-upload-btn").addEventListener("click", function(e){
        e.preventDefault();

        document.getElementById("video-upload-btn").setAttribute("disabled", "true");

        var videof = document.getElementById('video_file'),
            videopb = document.getElementById('video-bar-fill-id'),
            videopt = document.getElementById('video-bar-fill-text-id');

        app.videouploader({
            videoFiles: videof,
            videoProgressBar: videopb,
            videoProgressText: videopt,
            videoProcessor: "upload.php",

            finished: function(data){
                console.log(data);

            },

            error: function(){
                console.log("error");
            }
        });
    });
</script>

SERVER SIDE ::

<?php
    if(!empty($_FILES["video_file"]))
    {
        if(!empty($_FILES["video_file"]["error"]))
        {
            if(move_uploaded_file($_FILES["video_file"]["tmp_name"], __DIR__."/".$_FILES["video_file"]["name"] ))
            {
                echo "success";
            }
            else
            {
                echo "failed";
            }
        }
        else
        {
            echo "error";
        }

    }
?>

同时更改下面列出的php ini值。

  1. 的post_max_size
  2. 的upload_max_filesize
  3. 如果您在linux / ubuntu中 - 请按照以下步骤操作

    Open php ini file - 
    sudo nano /etc/php5/apache2/php.ini
    
    Update these values-
    post_max_size = 6000M
    upload_max_filesize = 6000M
    
    restart apache
    sudo /etc/init.d/apache2 restart