如何恢复暂停或中断的文件上传

这是一篇由 Simon Speich 撰写的客座文章。Simon 是一位 Web 开发人员,相信 Web 标准,并且从 Mozilla 0.8 开始就热爱 Mozilla(!)。

今天,Simon 正在试验 File API 和 Firefox 4 中引入的新 **Slice()** 方法。以下是他如何在文件上传器中实现 **恢复上传** 功能。

文件上传是使用 XHR Level2 对象 完成的。它提供了不同的方法和事件来处理请求(例如,发送数据和监视其进度)以及处理响应(例如,检查上传是否成功或发生错误)。有关更多信息,请阅读 如何开发 HTML5 图片上传器

不幸的是,XHR 对象没有提供暂停和恢复上传的方法。但是,可以通过将新的 File API 的 slice() 方法与 XHR 的 abort() 方法结合使用来实现该功能。让我们看看如何实现。

实时演示

您可以查看 实时文件上传器演示从 github.com 下载 JavaScript 和 PHP 代码

暂停和恢复上传

这个想法是为用户提供一个按钮来暂停正在进行的上传,并在稍后恢复它。暂停请求很简单。只需使用 abort() 方法中止请求。确保您的用户界面不会将其报告为错误。

更难的部分是恢复上传,因为请求已中止且连接已关闭。我们不重新发送整个文件,而是使用 blob 的 mozSlice() 方法首先创建一个包含文件剩余部分的块。然后,我们创建新的请求,发送块,并将其附加到请求中止之前已保存在服务器上的部分。

创建块

可以这样创建块:

var chunk = file.mozSlice(start, end);

我们只需要知道从哪里开始切片,也就是已经上传的字节数。最简单的方法是在我们中止请求之前保存 ProgressEvent 的 loaded 属性。但是,此数字不一定与服务器上写入的字节数完全相同。最可靠的方法是在我们再次上传之前,发送一个额外的请求以从服务器获取部分写入的文件的大小。然后,可以使用此信息对文件进行切片并创建块。

总结上述事件链

(假设上传已在进行中)

  1. 用户暂停上传
  2. UI 状态设置为暂停
  3. 上传被中止
  4. 服务器停止将文件写入磁盘
  5. 用户恢复上传
  6. UI 状态设置为恢复中
  7. 从服务器获取部分写入的文件大小
  8. 将文件切片成剩余部分(块)
  9. 上传块
  10. UI 状态设置为上传中
  11. 服务器附加数据

JavaScript 代码

// Assuming that the request to fetch the already written bytes has just
// taken place and xhr.result contains the response from the server.
var start = xhr.result.numWrittenBytes;
var chunk = file.mozSlice(start, file.size);

var req = new XMLHttpRequest();
req.open('post', 'fnc.php?fnc=resume', true);

req.setRequestHeader("Cache-Control", "no-cache");
req.setRequestHeader("X-Requested-With", "XMLHttpRequest");
req.setRequestHeader("X-File-Name", file.name);
req.setRequestHeader("X-File-Size", file.size);

req.send(chunk);

PHP 代码

服务器端处理正常上传和恢复上传之间的唯一区别在于,在后一种情况下,您需要附加到文件而不是创建文件。

$headers = getallheaders();
$protocol = $_SERVER[‘SERVER_PROTOCOL’];
$fnc = isset($_GET['fnc']) ? $_GET['fnc'] : null;
$file = new stdClass();
$file->name = basename($headers['X-File-Name']));
$file->size = $headers['X-File-Size']);

// php://input bypasses the php.ini settings, so we have to limit the file size ourselves:
$maxUpload = getBytes(ini_get('upload_max_filesize'));
$maxPost = getBytes(ini_get('post_max_size'));
$memoryLimit = getBytes(ini_get('memory_limit'));
$limit = min($maxUpload, $maxPost, $memoryLimit);
if ($headers['Content-Length'] > $limit) {
  header($protocol.' 403 Forbidden');
  exit('File size to big. Limit is '.$limit. ' bytes.');
}

$file->content = file_get_contents(’php://input’);
$flag = ($fnc == ‘resume’ ? FILE_APPEND : 0);
file_put_contents($file->name, $file->content, $flag);

function getBytes($val) {

$val = trim($val);
      $last = strtolower($val[strlen($val) - 1]);
      switch ($last) {
          case 'g': $val *= 1024;

case 'm': $val *= 1024;

case 'k': $val *= 1024;
      }

return $val;
}

注意!

上面的 PHP 代码示例没有进行任何安全检查。用户可以发送和写入任何类型的文件到您的磁盘,或者附加到或甚至覆盖您的任何文件。因此,在启用网站上传功能时,请务必采取适当的安全措施。

错误后恢复上传

暂停和恢复的事件序列也可用于在网络错误后继续上传。与其尝试重新上传整个文件,不如从服务器获取已写入的文件大小,并将文件首先切片成一个新的块。

关于恢复暂停或中断的文件上传的说明

将块附加到文件可能会创建损坏的文件,因为您无法控制服务器在请求中止后写入的内容——如果它确实写入任何内容。

浏览器崩溃后恢复上传

您可以将暂停和恢复功能更进一步。至少在理论上,即使在浏览器意外关闭或崩溃后,也可以恢复上传。问题在于,浏览器关闭后,读取到内存的文件对象会丢失。用户必须首先重新选择或拖动文件,然后才能对文件进行切片以恢复上传。

相反,您可以使用新的 IndexedDB API 并在进行任何上传之前存储文件。然后,在浏览器崩溃后,从数据库加载文件,切片成剩余的块并恢复上传。

关于 Simon Speich

Simon Speich 是一位 Web 开发人员,相信 Web 标准,并且从 Mozilla 0.8 开始就热爱 Mozilla。他还热衷于 摄影。您可以在他的网站 www.speich.net 上了解更多关于他的信息。

更多 Simon Speich 的文章…

关于 Paul Rouget

Paul 是一位 Firefox 开发人员。

更多 Paul Rouget 的文章…


18 条评论

  1. Tim Reynolds

    我担心新公开的 API 的安全隐患。关于“完成”上传作为附加恶意代码的一种方式?我认为,除了 API 之外,还应该开发一个标准的服务器端组件,以避免人们犯下天真的实现错误。

    2011 年 4 月 8 日 10:59

    1. voracity

      我不理解你的担忧。

      首先,新的 API 并没有引入任何操纵服务器的新功能,只有浏览器。

      其次,这不会鼓励大量新手开发者尝试使用花哨的文件上传器。如果说有什么不同的话,那就是它使新手开发者 *不太可能* 尝试这种事情,因为这些 API 使创建“最先进的”文件上传器变得困难,包括所有炫酷的功能(如进度条、缩略图预览、恢复等)。因此,第三方很可能会介入并提供即插即用的库来简化这些操作,就像 HTML 编辑器甚至简单的图片缩放(例如 Lightbox 等)一样。然后,这些第三方将 *完全* 做到你(以及可能还有 Ted)所要求的:创建标准的、安全的服务器端组件。

      让我们不要让浏览器制造商负责 *所有* 的创新:Web 库开发者也可以(并且正在)贡献自己的力量。

      2011 年 4 月 9 日 03:59

      1. voracity

        附注:很高兴再次在 hacks.mozilla.org 上看到实际的代码。除了 Wiki Wednesday 文章之外,如果此处每篇文章都要求至少提供 1 行代码,那就太好了。

        2011 年 4 月 9 日 04:03

  2. Tony Mechelynck

    为了解决 Tim Reynolds 的担忧,以及我的一些其他担忧,应该有一种方法可以防止在文件在此期间发生更改时恢复上传。将部分保存的文件与本地文件开头相同长度的部分进行比较可能会很昂贵,但在 ADSL(下载速度远快于上传速度)的情况下,它可能仍然比从头开始重新上传快得多,值得考虑。另一种可能性(这并没有解决浏览器崩溃包括交流电源故障的情况)是在中止后立即保存部分上传的哈希值(一旦可以合理地假设最后一个部分缓冲区已写入远程存储),并将该哈希值保存到某个地方(可能在磁盘缓存中),我们希望即使在重新启动后也能从中获取它。

    2011 年 4 月 8 日 11:47

  3. Bazzargh

    如果我没记错的话,浏览器在崩溃后也不处理恢复下载——我很久以前提交了一个关于此错误的补丁,现在可能已经腐烂了,但无法运行它的单元测试
    https://bugzilla.mozilla.org/show_bug.cgi?id=435799
    …如果有人有动力去处理它…
    随着 Firefox 在后续版本中变得更加稳定,它对我来说变得不那么重要了。

    2011 年 4 月 8 日 13:39

  4. Jeremy Walton

    我刚刚制作了一个几乎相同的上传器。不同的是,我没有进行完整上传,而是将文件分成块进行上传。首先,因为这样我就可以将 PHP 设置为具有极小的帖子大小。这允许 PHP 具有更小的内存占用量。其次,对于 JavaScript 中的小块,我计算块的 SHA1,这保证了块被正确发送。两个缺点是,根据块大小和您正在服务的客户端,您的上传速度可能会稍微慢一些,并且您上传的数据会稍微多一些,因为对于每个块,您都会发送新的 HTTP 标头。这也允许我暂停和恢复文件。添加完整文件的 sha1,以及文件名和大小,即使他们关闭浏览器,您也可以恢复文件。

    2011 年 4 月 11 日 22:32

  5. Yansky

    “相反,您可以使用新的 IndexedDB API 并在进行任何上传之前存储文件”

    你能解释一下吗?你的意思是你可以将实际的文件作为 blob 存储在 IndexedDB 中,还是说你可以存储对文件的引用?

    谢谢。

    2011 年 4 月 12 日 02:16

  6. Simon

    @Yansky:您不能将二进制数据直接作为 blob 存储在 indexedDB 中,因为它只允许您存储 JavaScript 对象。但是,您可以使用 FileReader API 将文件首先转换为字符串,然后将其作为对象的属性与文件大小和文件名一起存储在 DB 中。但这可能不适用于大型文件,但我还没有时间进行测试。

    仅存储文件引用不会有任何帮助,因为用户必须自己重新访问文件。

    2011 年 4 月 12 日 06:50

    1. Simon Speich

      即将推出的 Firefox 11 将能够直接在 indexedDB 中存储文件,请参阅 https://bugzilla.mozilla.org/show_bug.cgi?id=712621#c3

      2012 年 2 月 9 日 11:01

      1. Simon Speich

        hacks.mozilla.org 上有一篇新的文章解释了如何在 IndexedDB 中存储文件:在 IndexedDB 中存储图像和文件。这可以用来在浏览器崩溃后恢复上传。

        2012 年 2 月 26 日 03:22

  7. Davit

    您是在自己创建这些 X-* 标头(如 X-File-Name),还是在某个地方概述(推荐)了它们?

    2011 年 8 月 19 日 03:54

  8. Simon Speich

    X-something 表示自定义标头,因此是的,它们是编造的,但特别是 X-Requested-With 被广泛使用。

    2011 年 8 月 19 日 23:43

  9. Alejandro Invertir en bolsa

    非常棒的信息。我的网络连接不太好,所以我会尝试实施这个想法。

    2011 年 9 月 9 日 16:21

  10. Almas

    您好,
    我无法从我的桌面上拖放图片。当我拖放它时,图片只是在我的浏览器中显示。然后上传系统消失了。

    我可以通过浏览图像而不是拖放来获取演示吗?

    任何帮助?

    谢谢
    Almas

    2012 年 1 月 5 日 05:05

  11. Simon Speich

    演示目前无法使用。我将尽快修复它并通知您。

    2012 年 1 月 6 日 01:17

  12. Simon Speich

    演示已恢复正常。对由此造成的不便,我们深感抱歉。
    我更新了演示以使用 dojo 1.7.1,并且切换到使用 dojo 的新 AMD 加载程序。

    2012 年 1 月 7 日 10:20

  13. Almas

    您好,

    拖放正在工作。感谢您的修复。但是它可以用于多文件选择吗?当单击上传按钮时,所有文件上传都应同时启动。从那里我可以暂停单个文件。或者,如果任何互联网连接中断,则所有上传都应暂停。并且当网络连接可用时,这些应恢复。

    谢谢

    2012 年 1 月 7 日 10:46

  14. filkor

    这对我不起作用 :( 所以我制作了自己的演示…

    http://dnduploader.filkor.org/

    – 关闭浏览器或断开互联网连接,您可以回来并拖放相同的文件以继续上传过程
    – 多个文件
    – 所有文件在点击上传按钮时开始。
    – 您也可以暂停单个文件
    – Github 上的源代码

    2012 年 6 月 30 日 14:29

本文的评论已关闭。