Detect when user accepts to download a file

18,205

Solution 1

As i've found from years of maintaining download.js, there simply is no way to tell from JS (or likely in general, see below) what a user chooses to do with the download Open/Save dialog. It's a common feature request, and i've looked into it repeatedly over the years. I can say with confidence that it's impossible; I'll joyfully pay 10 times this bounty if someone can demo a mechanical way to determine the post-prompt user action on any file!

Further, it's not really just a matter of JS rules, the problem is complicated by the way browsers download and prompt such files. This means that even servers can't always tell what happened. There might be some specific work-arounds for a few specific cases, but they are not pretty or simple.

You could force your users to "re-upload" a downloaded file with an <input type=file> to validate it, but that's cumbersome at best, and the local file browse dialog could be alarming to some. It's the only sure-fire method to ensure a download, but for non-sensitive applications its very draconian, and it won't work on some "mobile" platforms that lack file support.

You might also try watching from the server side, pushing a message to the client that the file was hit on the server. The problem here is that downloads start downloading as soon as the Open/Save dialog appears, though invisibly in the background. That's why if you wait a few moments to "accept" a large file, it seems to download quickly at first. From the server's perspective, the activity is the same regardless of what the user does.

For a huge file, you could probably detect that the whole file was not transferred, which implies the user clicked "cancel", but it's a complicated syncing procedure pushing the status from backend to client. It would require a lot of custom programming with sockets, PHP message passing, EventSource, etc for little gain. It's also a race against time, and an uncertain amount of time at that; and slowing down the download is not recommended for user satisfaction.

If it's a small file, it will physically download before the user even sees the dialog, so the server will be useless. Also consider that some download manager extensions take over the job, and they are not guaranteed to behave the same as a vanilla browser. Forcing a wait can be treacherous to someone with a slow hard drive that takes "forever" to "finish" a download; we've all experienced this, and not being able to continue while the "spinny" winds down would lower user satisfaction, to put it mildly.

In short, there's no simple way, and really no way in general, except for huge files you know will take a long time to download. I've spent a lot of blood sweat and tears trying to provide my download.js users the ability, but there are simply no good options. Ryan dahl initially wrote node.js so he could provide his users an upload progress bar, maybe someone will make a server/client package to make it easy to do the same for downloads.

Solution 2

@dandavis

I'll joyfully pay 10 times this bounty if someone can demo a mechanical way to determine the post-prompt user action on any file!

Are you ready? cuz here are one hacky solution 😉

My StreamSaver lib don't use blob's to download a file with a[download]. It uses service worker to stream something to the disc by emulating how the server handles download with content-disposition attachment header.

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

Now you don't have any exact way of knowing what the user pressed in the dialog but you have some information about the stream.
If the user press cancel in the dialog or abort the ongoing download then the stream gets aborted too.

The save button is trickier. But lets begin with what a stream bucket highWaterMark can tell us.

In my torrent example I log the writer.desiredSize. It's the correlation to how much data it is willing to receive. When you write something to the stream it will lower the desired size (whether it be a count or byte strategy). If it never increases then it means that user probably have paused the download. When it goes down below 0 then you are writing more data than what the user is asking for.

And every chunk write you do returns a promise

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

That promise will resolve when the bucket isn't full. But it dose not mean that the chunk have been written to the disc yet, it only means that the chunk has been passed from the one stream to another stream (from write -> readable -> respondWith) and will often do so in the beginning of the stream and when another earlier chunk have been written to the disc.

It's a possibility that the write stream can finishes even before the user makes a choice if the hole data can fit within the bucket (memory)

tweking the bucket size to be lower then the data can help

So you can make assumption on when the - download starts - finish - and pauses but you won't know for sure since you don't get any events (apart from the abort that closes the stream)

Note that the torrent example don't show correct size if you don't have support for Transferable streams but you could get around this if you do everything inside a service worker. (instead of doing it in the main thread)

detecting when the stream finish is as easy as

readableStream.pipeTo(fileStream).then(done)

And for future references WICG/native-file-system might give you access to write files to disc but it has to resolve a prompt dialog promise before you can continue and might be just what the user is asking for

There are examples of saving a blob as a stream, and even more multiple blob's as a zip too if you are interested

Solution 3

Given that user is, or should be aware that file should be downloaded before next step in process, user should expect some form of confirmation that file has been downloaded to occur.

You can create a unique idenfifier or timestamp to include within downloaded file name by utilizing <a> element with download attribute set to a the modified file name.

At click event of <button> element call .click() on <a> element with href set to a Blob URL of file. At a element click handler call .click() on an <input type="file"> element, where at attached change event user should select same file which was downloaded at the user action which started download of file.

Note the chaining of calls to .click() beginning with user action. See Trigger click on input=file on asynchronous ajax done().

If the file selected from user filesystem is equal to modified downloaded file name, call function, else notify user that file download has not been confirmed.

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

Share:
18,205
yitzih
Author by

yitzih

Updated on June 14, 2022

Comments

  • yitzih
    yitzih almost 2 years

    I want to redirect the user to a different webpage after they click a hyperlink which allows them to download a file. However since they need to make a choice in the open/save file dialog, I don't want to redirect them until they accept the download.

    How can I detect that they performed this action?