检测用户何时接受下载文件

IT技术 javascript browser download
2021-02-02 10:09:04

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

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

6个回答

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

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

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

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

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

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

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

@dandavis

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

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

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

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

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

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

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

你写的每一个块都会返回一个Promise

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

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

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

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

因此,您可以假设 - 下载开始 - 完成 - 和暂停的时间,但您不确定,因为您没有收到任何事件(除了关闭流的中止)

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

检测流何时结束就像

readableStream.pipeTo(fileStream).then(done)

对于未来的参考,WICG/native-file-system可能会让您访问将文件写入磁盘,但它必须在您继续之前解决提示对话框Promise,并且可能正是用户所要求的

有一些将blob保存为流的示例,如果您有兴趣,还可以将更多blob 保存为 zip

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

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

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

请注意.click()从用户操作开始的调用链请参阅触发单击 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

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

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

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

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

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

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

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

只是一些想法。