使用 Ajax 和 FileReader 分块写入文件

Writing File in Chunks using Ajax and FileReader

我正在创建一个文件上传器,其中的内容将使用第 3 方 API 以块的形式写入远程服务器。 API 提供了一个 WriteFileChunk() 方法,它有 3 个参数,目标文件路径,字节的起始位置(Int64)和数据(字符串)。

每次 FileReader 收到最大支持大小 (16kb) 的块时,我需要使用 Ajax 将其传递到 PHP 文件并使用 API 写入.我怀疑这应该在 FileReader 的 onprogress 事件中完成,但是我有点不知所措,因为我找不到任何类似的例子。

使用 FileReader 实现这一点的最佳方法是什么,以确保在写入下一个块之前上传每个块?如果onprogress是最好的选择,我怎样才能得到当前的区块数据?

$(document).ready(function()
{
    function uploadFile()
    {
        var files = document.getElementById('file').files;

        if (!files.length)
        {
            alert('Please select a file!');
            return;
        }

        var file = files[0];
        var first_byte = 0;
        var last_byte = file.size - 1;

        // File Reader
        var reader = new FileReader();
        reader.onerror = function(evt)
        {
            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:
                    break;
                    default:
                    alert('An error occurred reading this file.');
            };
        };
        reader.onprogress = function(evt)
        {
            if (evt.lengthComputable)
            {
                var percentLoaded = Math.round((evt.loaded / evt.total) * 100);
                console.log(percentLoaded + "%");

                if (percentLoaded < 100)
                {
                    $("#upload_progress").progressbar('value', percentLoaded);
                }
            }
        };
        reader.onabort = function(evt)
        {
            alert('File Upload Cancelled');
        };
        reader.onloadstart = function(evt)
        {
            $("#upload_progress").progressbar({
                value: 0,
                max: 100
            });
        };
        reader.onload = function(evt)
        {
            $("#upload_progress").progressbar('value', 100);
        };
        reader.onloadend = function(evt)
        {
            if (evt.target.readyState == FileReader.DONE) // DONE == 2
            {
                alert("Upload Complete!");
                //console.log(evt.target.result);
            }
        };

        var blob = file.slice(first_byte, last_byte + 1);
        reader.readAsBinaryString(blob);
    }

    fileupload_dialog = $( "#dialog-fileupload" ).dialog(
    {
        autoOpen: false,
        height: 175,
        width: 350,
        modal: true,
        buttons:
        {
            "Upload File": uploadFile
        },
        close: function()
        {
            form[ 0 ].reset();
        }
    });

    form = fileupload_dialog.find( "form" ).on( "submit", function( event )
    {
        event.preventDefault();
        uploadFile();
    });

    $("#file_upload a").click(function()
    {
        event.preventDefault();
        fileupload_dialog.dialog( "open" );
    });
});

这里的主要挑战是 FileReader 需要先将整个文件读入内存,然后 return 通过 result 属性 向我们提供任何可用数据,这意味着您无法在读取文件时从文件中获取块(并且进度事件不会 provide/point 任何数据):

This property [result] is only valid after the read operation is complete [...]

Source

由于整个文件都被加载到内存中,因此除了减少两个进程之间可能存在的延迟外,将读取过程分块(如果可能的话)实际上没有任何好处。

我会根据以上建议采用以下方法:

  • 将整个文件加载到内存中,但作为 ArrayBuffer
  • 计算所需的段数(Math.ceil(fileLength/chunkSize))
  • 使用 Uint8Array 视图为 ArrayBuffer 使用偏移量和块长度参数创建一个块
  • 发送块,异步等待响应,继续下一个块,直到剩余长度 <= 0 字节。

如果需要,块可以在发送之前转换为 Blob:

var chunkBlob = new Blob([chunk], {type: "application/octet-stream"});

示例过程

一个伪服务器示例,在每个 16kb 的块之间任意等待 100 毫秒:

file.onchange = function(e) {
  var fr = new FileReader();
  fr.onprogress = function(e) {progress.value = e.loaded / e.total};
  fr.onload = startUpload.bind(fr);
  progress.style.display = "inline-block";
  fr.readAsArrayBuffer(e.target.files[0]);
}

// Main upload code
function startUpload() {
  
  // calculate sizes
  var chunkSize = 16<<10;
  var buffer = this.result;
  var fileSize = buffer.byteLength;
  var segments = Math.ceil(fileSize / chunkSize);
  var count = 0;
  progress.value = 0;

  // start "upload"
  (function upload() {
    var segSize = Math.min(chunkSize, fileSize - count * chunkSize);
    if (segSize > 0) {
      var chunk = new Uint8Array(buffer, count++ * chunkSize, segSize); // get a chunk
      progress.value = count / segments;
      // send chunk to server (here pseudo cycle for demo purpose)
      setTimeout(upload, 100); // when upload OK, call function again for the next block
    }
    else {
      alert("Done");
      progress.style.display = "none";
   }
  })()
}
body {font:16px sans-serif;margin:20px 0 0 20px}
<label>Select any file: <input type=file id="file"></label><br>
<progress id="progress" value=0 max=1 style="display:none" />