如何使用 JavaScript 将二进制文件发送到服务器

时间:2021-01-20 20:48:59

标签: javascript ajax file xmlhttprequest

我正在为我的 Messenger 进行文件加密,但在加密完成后我正在努力上传文件。

加密在性能方面似乎不错,但是当我尝试上传时,浏览器完全挂起。 Profiler无限写入“小GC”事件,每10秒出现关于挂起脚本的黄色条。

我已经尝试过的:

  1. 用FileReader读取文件到ArrayBuffer,然后把它变成一个基本的Array,加密它,然后创建一个FormData对象,从数据中创建一个File,将其附加到FormData并发送。当我没有进行加密时,它可以快速处理大约 1.3 Mb 大小的原始、未触及的文件,但是在上传后加密的“假”文件对象上我得到了 4.7 Mb 的文件并且它不可用。

  2. 作为普通的 POST 字段发送(多部分表单数据编码)。用这种方式保存在PHP上后数据损坏了。

  3. 作为 Base64 编码的 POST 字段发送。最后,在我找到了一个从二进制数组到 Base64 字符串的快速转换函数后,它开始以这种方式工作。 btoa() 在编码/解码后给出了错误的结果。但是在我尝试了一个 8.5 Mb 大小的文件后,它又挂了。

  4. 我尝试将额外的数据移动到 URL 字符串,并按照 here 所述将文件作为 Blob 发送。没有成功,浏览器仍然挂起。

  5. 我尝试向 Blob 构造函数传递一个基本数组,一个由它组成的 Uint8Array,最后我尝试按照文档中的建议发送 File,但结果仍然相同,即使文件很小。

代码有什么问题?发生此挂起时,HDD 负载为 0%。而且有问题的文件真的很小

当我按下按钮紧急终止 JS 脚本时,这是我从服务器脚本中得到的输出:

Warning: Unknown: POST Content-Length of 22146226 bytes exceeds the limit of 8388608 bytes in Unknown on line 0

Warning: Cannot modify header information - headers already sent in Unknown on line 0

Warning: session_start() [function.session-start]: Cannot send session cache limiter - headers already sent in D:\xmessenger\upload.php on line 2
Array ( ) 

这是我的 JavaScript:

function uploadEncryptedFile(nonce) {
    if (typeof window['FormData'] !== 'function' || typeof window['File'] !== 'function') return
    
    var file_input = document.getElementById('attachment')
    
    if (!file_input.files.length) return
    var file = file_input.files[0]
    
    var reader = new FileReader();
    
    reader.addEventListener('load', function() {
        var data = Array.from(new Uint8Array(reader.result))
        var encrypted = encryptFile(data, nonce)
        //return  //Here it never hangs
        var form_data = new FormData()
        form_data.append('name', file.name)
        form_data.append('type', file.type)
        form_data.append('attachment', arrayBufferToBase64(encrypted))
        /* form_data.append('attachment', btoa(encrypted)) // Does not help */
        form_data.append('nonce', nonce)
        var req = getXmlHttp()
        req.open('POST', 'upload.php?attachencryptedfile', true)
        req.onload = function() {
            var data = req.responseText.split(':')
            document.getElementById('filelist').lastChild.realName = data[2]
            document.getElementById('progress2').style.display = 'none'
            document.getElementById('attachment').onclick = null
            encryptFilename(data[0], data[1], data[2])
        }
        req.send(form_data)
        /* These lines also fail when the file is larger */
        /* req.send(new Blob(encrypted)) */
        /* req.send(new Blob(new Uint8Array(encrypted))) */
    })
    reader.readAsArrayBuffer(file)
}

function arrayBufferToBase64(buffer) {
    var binary = '';
    var bytes = new Uint8Array(buffer);
    var len = bytes.byteLength;
    for (var i = 0; i < len; i++) {
        binary += String.fromCharCode(bytes[i]);
    }
    return window.btoa(binary);
}

这是我的 PHP 处理程序代码:

if (isset($_GET['attachencryptedfile'])) {
    $entityBody = file_get_contents('php://input');
    if ($entityBody == '') exit(print_r($_POST, true)); 
    else exit($entityBody);
    
    if (!isset($_POST["name"])) exit("Error");
    
    $name = @preg_replace("/[^0-9A-Za-z._-]/", "", $_POST["name"]);
    $nonce = @preg_replace("/[^0-9A-Za-z+\\/]/", "", $_POST["nonce"]);
    
    if ($name == ".htaccess") exit();

    $data = base64_decode($_POST["attachment"]);
    //print_r($_POST);
    //exit();
    if (strlen($data)>1024*15*1024) exit('<script type="text/javascript">parent.showInfo("Файл слишком большой"); parent.document.getElementById(\'filelist\').removeChild(parent.document.getElementById(\'filelist\').lastChild); parent.document.getElementById(\'progress2\').style.display = \'none\'; parent.document.getElementById(\'attachment\').onclick = null</script>');
    $uname = uniqid()."_".str_pad($_SESSION['xm_user_id'], 6, "0", STR_PAD_LEFT).substr($name, strrpos($name, "."));
    file_put_contents("upload/".$uname, $data);
    mysql_query("ALTER TABLE `attachments` AUTO_INCREMENT=0");
    mysql_query("INSERT INTO `attachments` VALUES('0', '".$uname."', '".$name."', '0', '".$nonce."')");
    exit(mysql_insert_id().":".$uname.":".$name);
}

HTML 表单:

<form name="fileForm" id="fileForm" method="post" enctype="multipart/form-data" action="upload.php?attachfile" target="ifr">
    <div id="fileButton" title="Прикрепить файл" onclick="document.getElementById('attachment').click()"></div>
    <input type="file" name="attachment" id="attachment" title="Прикрепить файл" onchange="addFile()" />
</form>

1 个答案:

答案 0 :(得分:1)

UPD:不幸的是,问题没有解决。我的回答只是部分正确。现在我在代码中犯了一个愚蠢的错误(忘记更新服务器端),我发现了另一个可能挂起的原因。如果我提交一个基本的 POST 表单 (x-www-urlencoded) 并且 PHP 脚本中的代码尝试执行这一行($uname 已定义,$_FILES 是一个空数组)

if (!copy($_FILES['attachment']['tmp_name'], "upload/".$uname)) exit("Error");

然后整个事情又挂了。如果我终止脚本,服务器响应是代码 200,并且正文内容很好(我的开发机器上有错误输出)。我知道这是一件坏事 - 用第一个完全未定义的参数调用 copy,但即使服务器错误 500 也不能以这种方式挂起浏览器(顺便说一句,新的最新版本的 Firefox 也受到影响) .

我在 Windows 7 x64 和 PHP 5.3 上安装了 Apache 2.4。有人可以验证这件事吗?也许应该向 Apache/Firefox 团队提交错误?


天啊。这种可怕的行为是由 php.ini 中设置的... post_max_size = 8M 引起的。我发现小于 8 Mb 的文件实际上并没有挂起浏览器。

最后一个问题是 - 为什么?为什么 Apache/PHP(顺便说一句,我有 Apache 2.4,它不旧)以某种方式优雅地中止连接,告诉浏览器超出了限制?或者它可能是 XHR 实现中的一个错误,不适用于基本表单提交。无论如何,对偶然发现它的人很有用。

顺便说一下,我在 Chrome 中尝试了相同的 POST 大小限制,但它并没有像在 Firefox 中那样完全挂在那里(请求仍然处于某种挂起状态,“无响应”,但 JS 引擎并且 UI 没有被阻止)。