【问题标题】:AJAX/PHP based upload with progress bar for large files基于 AJAX/PHP 的大文件上传,带有进度条
【发布时间】:2012-04-20 19:23:03
【问题描述】:

我一直在尝试创建一个非 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.loaded 以 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')
);

?>

【问题讨论】:

  • 查看 php.ini 选项,寻找 max_upload_file_size max_post_size 和相关标志以发布/文件大小上传。
  • 我已经检查过了,一切似乎都很好。
  • 这听起来像个笑话,但有时人们会做一些傻事,所以我还是问了一句:“你是在使用 FAT32 作为文件系统吗?”
  • 您已经解决问题了吗?如果是,您能否分享您的解决方案?如果没有 - 例如使用 jQuery 的 blob 函数将文件分成小部分怎么样?

标签: php ajax file upload progress


【解决方案1】:

其他人已经指出,在任何配置正确的 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 的浏览器才支持分块文件上传,其中包括 Google Chrome 和 Mozilla Firefox 4+ -- https://github.com/blueimp/jQuery-File-Upload/wiki/Chunked-file-uploads

要在 Mozilla Firefox 4-6(Firefox 7 之前支持 XHR 上传的 Firefox 版本)中进行分块上传,还必须将 multipart 选项设置为 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 的文件,但 IE 低于 9 的版本有一个错误,无法处理大于 2GB 的文件。

【讨论】:

    【解决方案2】:

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

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

        【讨论】:

          【解决方案5】:

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

          你可能想试试这样的东西:

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

          干杯

          【讨论】:

            【解决方案6】:

            我尝试使用 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;
            }
            

            阿贾克斯 ::

            <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>
            

            服务器端::

            <?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

            如果您使用的是 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
            

            【讨论】:

            • 这不是要走的路。如果您在上传快要结束时遇到错误,那么您已经失去了一切。最好的办法还是把你的上传分块,每个块分别上传
            猜你喜欢
            • 2012-07-15
            • 2011-11-06
            • 1970-01-01
            • 2014-03-03
            • 1970-01-01
            • 2014-06-03
            • 2012-04-10
            • 1970-01-01
            相关资源
            最近更新 更多