【问题标题】:Detect when user accepts to download a file检测用户何时接受下载文件
【发布时间】:2016-12-26 18:18:33
【问题描述】:

我想在用户单击允许他们下载文件的超链接后将其重定向到不同的网页。但是,由于他们需要在打开/保存文件对话框中做出选择,我不想在他们接受下载之前重定向他们。

我如何才能检测到他们执行了此操作?

【问题讨论】:

  • 浏览器无法挂接到文件保存对话框。这是由于明显的安全问题
  • 很遗憾你做不到,因为那个界面在浏览器之外,所以你处理不了!!
  • 正如之前所说的那样,这是一个安全问题,但是您可以找到并想到很多解决方法,我在 SO stackoverflow.com/a/4168965/20126 上看到了这个
  • 根据文件的不同,您可以挑战文件的一部分,例如“文件的第三个单词是什么”?

标签: javascript browser download


【解决方案1】:

正如我在多年维护download.js 中发现的那样,根本没有办法告诉 JS(或者一般情况下,见下文)用户选择下载的内容是 打开/保存对话框。这是一个常见的功能请求,多年来我反复研究过它。我可以自信地说这是不可能的;如果有人可以演示一种机械方法来确定提示后用户对任何文件的操作,我会很乐意支付 10 倍的赏金!

此外,这不仅仅是 JS 规则的问题,浏览器下载和提示此类文件的方式使问题变得复杂。这意味着即使是服务器也不能总是告诉发生了什么。对于一些特定情况,可能有一些特定的解决方法,但它们并不漂亮或简单。

您可以强制您的用户“重新上传”带有<input type=file> 的下载文件以对其进行验证,但这充其量是很麻烦的,而且本地文件浏览对话框可能会让某些人感到震惊。这是确保下载的唯一可靠方法,但对于非敏感应用程序而言,它非常严格,并且在某些缺乏文件支持的“移动”平台上无法运行。

您也可以尝试从服务器端观看,向客户端推送一条消息,说明文件在服务器上被命中。这里的问题是,一旦“打开/保存”对话框出现,下载就会开始下载,尽管在后台是不可见的。这就是为什么如果您等待片刻“接受”一个大文件,一开始它似乎下载得很快。从服务器的角度来看,无论用户做什么,活动都是相同的。

对于一个大文件,您可能会检测到整个文件传输,这意味着用户单击“取消”,但这是一个复杂的同步过程,将状态从后端推送到客户端。它需要大量使用套接字、PHP 消息传递、EventSource 等的自定义编程,但收效甚微。这也是一场与时间的赛跑,而且时间不确定。为了让用户满意,不建议放慢下载速度。

如果它是一个小文件,它会在用户看到对话框之前物理下载,因此服务器将无用。还要考虑一些下载管理器扩展接管了这项工作,并且不能保证它们的行为与普通浏览器相同。强迫等待对于硬盘驱动器缓慢“永远”“完成”下载的人来说可能是危险的。我们都经历过这种情况,委婉地说,在“spinny”结束时无法继续会降低用户满意度。

简而言之,没有简单的方法,也没有一般的方法,除了你知道需要很长时间才能下载的大文件。我花了很多血汗和泪水试图为我的 download.js 用户提供这种能力,但根本没有好的选择。 Ryan dahl 最初编写 node.js 是为了给他的用户提供一个上传进度条,也许有人会制作一个服务器/客户端包以方便下载。

【讨论】:

    【解决方案2】:

    @dandavis

    如果有人可以演示一种机械方法来确定提示后用户对任何文件的操作,我会很乐意支付 10 倍的赏金!

    你准备好了吗?因为这里有一个 hacky 解决方案?

    我的 StreamSaver 库不要使用 blob 下载带有 a[download] 的文件。它使用 service worker 通过模拟服务器如何使用 content-disposition 附件标头处理下载来将内容流式传输到光盘。

    evt.respondWith(
      new Response(
        new ReadableStream({...})
      )
    )
    

    现在您无法确切知道用​​户在对话框中按了什么,但您有一些关于流的信息。
    如果用户在对话框中按取消或中止正在进行的下载,则流将获取也流产了。

    保存按钮比较复杂。但是让我们从流桶 highWaterMark 可以告诉我们的内容开始。

    在我的torrent example 中,我记录了writer.desiredSize。这与它愿意接收多少数据有关。当您向流中写入内容时,它会降低所需的大小(无论是计数策略还是字节策略)。如果它从未增加,则意味着用户可能已暂停下载。当它低于 0 时,您写入的数据比用户要求的要多。

    你所做的每一个块写入都会返回一个承诺

    writer.getWriter().write(uint8).then(() => {
      // Chunk have been sent to the destination bucket
      // and desiredSize increase again 
    })
    

    当桶未满时,该承诺将解决。但这并不意味着该块已被写入磁盘,它仅意味着该块已从一个流传递到另一个流(从写入 -> 可读 -> 响应),并且通常会在开始时这样做当另一个更早的块被写入磁盘时。

    如果空洞数据可以放入存储桶(内存)中,甚至在用户做出选择之前写入流就有可能完成

    将存储桶大小调整为小于数据可以提供帮助

    所以你可以假设 - 下载开始 - 结束 - 并暂停 但您无法确定,因为您没有收到任何事件(除了关闭流的中止)

    请注意,如果您不支持 Transferable streams,则 torrent 示例不会显示正确的大小,但如果您在 service worker 中执行所有操作,则可以解决此问题。 (而不是在主线程中进行)

    检测流何时结束非常简单

    readableStream.pipeTo(fileStream).then(done)
    

    对于将来的参考,WICG/native-file-system 可能会授予您将文件写入光盘的权限,但它必须先解决提示对话框承诺,然后才能继续,并且可能正是用户要求的内容

    有将blob 保存为流的示例,如果您有兴趣,还有更多multiple blob's as a zip

    【讨论】:

      【解决方案3】:

      鉴于用户已经或应该知道文件应该在下一步之前下载,用户应该期待某种形式的文件已下载确认。

      您可以使用<a> 元素将download 属性设置为修改后的文件名,从而创建一个唯一标识符或时间戳以包含在下载的文件名中。

      <button> 元素的click 事件中,在<a> 元素上调用.click(),其中href 设置为文件的Blob URL。在 a 元素 click 处理程序调用 .click()<input type="file"> 元素上,在附加的 change 事件中,用户应该选择在开始下载文件的用户操作中下载的相同文件。

      注意以用户操作开始的对.click() 的调用链。见Trigger click on input=file on asynchronous ajax done()

      如果从用户文件系统中选择的文件等于修改后的下载文件名,则调用函数,否则通知用户文件下载尚未确认。

      window.addEventListener("load", function() {
        
        let id, filename, url, file; 
        let confirmed = false;
        const a = document.querySelector("a");
        const button = document.querySelector("button");
        const confirm = document.querySelector("input[type=file]");
        const label = document.querySelector("label");
      
        function confirmDownload(filename) {
          if (confirmed) {
            filename = filename.replace(/(-\d+)/, "");
            label.innerHTML = "download of " + filename + " confirmed";
          } else {
            confirmed = false;
            label.innerHTML = "download not confirmed";
          }
          URL.revokeObjectURL(url);
          id = url = filename = void 0;
          if (!file.isClosed) {
            file.close()
          }
        }
      
        function handleAnchor(event) {
          confirm.click();
          label.innerHTML = "";
          confirm.value = "";
          window.addEventListener("focus", handleCancelledDownloadConfirmation);
        }
      
        function handleFile(event) {
          if (confirm.files.length && confirm.files[0].name === filename) {
            confirmed = true;      
          } else {
            confirmed = false;
          }
          confirmDownload(filename);
        }
      
        function handleDownload(event) {
          // file
          file = new File(["abc"], "file.txt", {
            type: "text/plain",
            lastModified: new Date().getTime()
          });
          id = new Date().getTime();
          filename = file.name.match(/[^.]+/g);
          filename = filename.slice(0, filename.length - 1).join("")
                     .concat("-", id, ".", filename[filename.length - 1]);
          file = new File([file], filename, {
            type: file.type,
            lastModified: id
          });
          a.download = filename;
          url = URL.createObjectURL(file);
          a.href = url;
          alert("confirm download after saving file");
          a.click();
        }
        
        function handleCancelledDownloadConfirmation(event) {
          if (confirmed === false && !confirm.files.length) {
            confirmDownload(filename);
          }
          window.removeEventListener("focus", handleCancelledDownloadConfirmation);
        }
      
        a.addEventListener("click", handleAnchor);
      
        confirm.addEventListener("change", handleFile);
      
        button.addEventListener("click", handleDownload);
      
      });
      <button>download file</button>
      <a hidden>download file</a>
      <input type="file" hidden/>
      <label></label>

      plnkr http://plnkr.co/edit/9NmyiiQu2xthIva7IA3v?p=preview

      【讨论】:

      • 对此的一些想法:考虑验证日期和大小,这很容易使用元数据(没有 FileReader)。更安全的是对文件进行 MD5 处理并与服务器提供的值进行比较。或者,在下载后压缩的文本文件中包含密码。
      • @dandavis 是的,如前所述,程序可以调整、改进、完善,甚至可能加强。答案的方法是概念证明的粗略草稿,用户了解并与流程合作。有时是这样;尽管人们经常试图找到绕过程序的方法。
      • 考虑到给定的条件,我认为这个解决方案很酷,尽管我只会在极少数情况下使用它。毕竟,这对用户来说非常尴尬。
      【解决方案4】:

      jquery.fileDownload 允许您这样做:

      $(document).on("click", "a.fileDownloadPromise", function () {
          $.fileDownload($(this).prop('href'))
              .done(function () { alert('File download a success!'); })
              .fail(function () { alert('File download failed!'); });
      
          return false; 
      });
      

      看看 Github:

      https://github.com/johnculviner/jquery.fileDownload

      【讨论】:

      • 对不起,但我认为在 plunker 或 jsfiddle 上不可能。要运行此解决方案,您必须在同一域中托管可下载文件,并且必须在下载响应中返回 cookie。
      • 这个库只能判断是否存在服务器错误尝试下载它,而不是用户对对话框做了什么。仅仅因为一个文件是 200ing 并不意味着他们正在保存或打开它。
      【解决方案5】:

      我最近涉足了一个项目,该项目要求我指定用户是否可以上传特定类型的文件,即(用户可以上传 png 但不能上传 pdf)。

      我可能没有使用最有效的方法,但最终我所做的是编写一个小型的内置“webapp”,用作文件浏览器,用于上传或下载。

      我想在不发布我的“秘密项目”的情况下最接近的例子是https://encodable.com/filechucker/

      也许您可以编写一个简单的集成文件浏览器,例如云服务有时使用的文件浏览器(即保管箱),并具有一些使用自定义框和东西检测输入的功能。

      只是一些想法。

      【讨论】:

        【解决方案6】:

        试试这个:

        <html>
        <head>
        <script type="text/javascript">
            function Confirmation(pg) {
                var res = confirm("Do you want to download?");
                if(res){
                    window.open(pg,"_blank");
                }
                return res;
            }
        </script>
        </head>
        <body>
          <a href="http://yoursite/download.zip" onclick="return Confirmation('http://yoursite/redirect.html');">download</a>
        </body>
        </html>
        

        【讨论】:

        • 方法是否强制下载文件?
        • 抱歉,您不能强制客户端下载文件。
        • 此代码让您的客户做出选择,如果他们接受下载您的文件,他们将重定向到目标 URL,否则他们无法访问目标 URL。但你不能强迫他们下载。
        • 现在的问题是如何检测文件是否被客户端实际下载,是吗?
        • 当用户接受下载文件时检测没有问题。它不是强制
        猜你喜欢
        • 2015-06-29
        • 2011-09-20
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多