Detect When a Browser Receives a File Download

Detect when a browser receives a file download

One possible solution uses JavaScript on the client.

The client algorithm:

  1. Generate a random unique token.
  2. Submit the download request, and include the token in a GET/POST field.
  3. Show the "waiting" indicator.
  4. Start a timer, and every second or so, look for a cookie named "fileDownloadToken" (or whatever you decide).
  5. If the cookie exists, and its value matches the token, hide the "waiting" indicator.

The server algorithm:

  1. Look for the GET/POST field in the request.
  2. If it has a non-empty value, drop a cookie (e.g. "fileDownloadToken"), and set its value to the token's value.

Client source code (JavaScript):

function getCookie( name ) {
var parts = document.cookie.split(name + "=");
if (parts.length == 2) return parts.pop().split(";").shift();
}

function expireCookie( cName ) {
document.cookie =
encodeURIComponent(cName) + "=deleted; expires=" + new Date( 0 ).toUTCString();
}

function setCursor( docStyle, buttonStyle ) {
document.getElementById( "doc" ).style.cursor = docStyle;
document.getElementById( "button-id" ).style.cursor = buttonStyle;
}

function setFormToken() {
var downloadToken = new Date().getTime();
document.getElementById( "downloadToken" ).value = downloadToken;
return downloadToken;
}

var downloadTimer;
var attempts = 30;

// Prevents double-submits by waiting for a cookie from the server.
function blockResubmit() {
var downloadToken = setFormToken();
setCursor( "wait", "wait" );

downloadTimer = window.setInterval( function() {
var token = getCookie( "downloadToken" );

if( (token == downloadToken) || (attempts == 0) ) {
unblockSubmit();
}

attempts--;
}, 1000 );
}

function unblockSubmit() {
setCursor( "auto", "pointer" );
window.clearInterval( downloadTimer );
expireCookie( "downloadToken" );
attempts = 30;
}

Example server code (PHP):

$TOKEN = "downloadToken";

// Sets a cookie so that when the download begins the browser can
// unblock the submit button (thus helping to prevent multiple clicks).
// The false parameter allows the cookie to be exposed to JavaScript.
$this->setCookieToken( $TOKEN, $_GET[ $TOKEN ], false );

$result = $this->sendFile();

Where:

public function setCookieToken(
$cookieName, $cookieValue, $httpOnly = true, $secure = false ) {

// See: http://stackoverflow.com/a/1459794/59087
// See: http://shiflett.org/blog/2006/mar/server-name-versus-http-host
// See: http://stackoverflow.com/a/3290474/59087
setcookie(
$cookieName,
$cookieValue,
2147483647, // expires January 1, 2038
"/", // your path
$_SERVER["HTTP_HOST"], // your domain
$secure, // Use true over HTTPS
$httpOnly // Set true for $AUTH_COOKIE_NAME
);
}

Detect when user accepts to download a file

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.

Detect and notify when download is complete

I have updated the below reference code. Add a proper download url link and try this out.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style type="text/css">
body {
padding: 0;
margin: 0;
}

svg:not(:root) {
display: block;
}

.playable-code {
background-color: #f4f7f8;
border: none;
border-left: 6px solid #558abb;
border-width: medium medium medium 6px;
color: #4d4e53;
height: 100px;
width: 90%;
padding: 10px 10px 0;
}

.playable-canvas {
border: 1px solid #4d4e53;
border-radius: 2px;
}

.playable-buttons {
text-align: right;
width: 90%;
padding: 5px 10px 5px 26px;
}
</style>

<style type="text/css">
.event-log {
width: 25rem;
height: 4rem;
border: 1px solid black;
margin: .5rem;
padding: .2rem;
}

input {
width: 11rem;
margin: .5rem;
}

</style>

<title>XMLHttpRequest: progress event - Live_example - code sample</title>
</head>
<body>

<div class="controls">
<input class="xhr success" type="button" name="xhr" value="Click to start XHR (success)" />
<input class="xhr error" type="button" name="xhr" value="Click to start XHR (error)" />
<input class="xhr abort" type="button" name="xhr" value="Click to start XHR (abort)" />
</div>

<textarea readonly class="event-log"></textarea>


<script>
const xhrButtonSuccess = document.querySelector('.xhr.success');
const xhrButtonError = document.querySelector('.xhr.error');
const xhrButtonAbort = document.querySelector('.xhr.abort');
const log = document.querySelector('.event-log');

function handleEvent(e) {
if (e.type=='progress')
{log.textContent = log.textContent + `${e.type}: ${e.loaded} bytes transferred Received ${event.loaded} of ${event.total}\n`;
}
else if (e.type=='loadstart')
{
log.textContent = log.textContent + `${e.type}: started\n`;
}
else if (e.type=='error')
{
log.textContent = log.textContent + `${e.type}: error\n`;
}
else if (e.type=='loadend')
{
log.textContent = log.textContent + `${e.type}: completed\n`;
}

}

function addListeners(xhr) {
xhr.addEventListener('loadstart', handleEvent);
xhr.addEventListener('load', handleEvent);
xhr.addEventListener('loadend', handleEvent);
xhr.addEventListener('progress', handleEvent);
xhr.addEventListener('error', handleEvent);
xhr.addEventListener('abort', handleEvent);
}

function runXHR(url) {
log.textContent = '';

const xhr = new XMLHttpRequest();

var request = new XMLHttpRequest();
addListeners(request);
request.open('GET', url, true);
request.responseType = 'blob';
request.onload = function (e) {
var data = request.response;
var blobUrl = window.URL.createObjectURL(data);
var downloadLink = document.createElement('a');
downloadLink.href = blobUrl;
downloadLink.download ='download.zip';
downloadLink.click();
};
request.send();
return request
}

xhrButtonSuccess.addEventListener('click', () => {
runXHR('https://abbbbbc.com/download.zip');
});

xhrButtonError.addEventListener('click', () => {
runXHR('http://i-dont-exist');
});

xhrButtonAbort.addEventListener('click', () => {
runXHR('https://raw.githubusercontent.com/mdn/content/main/files/en-us/_wikihistory.json').abort();
});
</script>

</body>
</html>

Browser event when downloaded file is saved to disk

There's no such browser event in JavaScript and even if there was you can not trust the user's browser to provide security for you.

You're better off using a GUID to generate a unique URL for each download. You can then for example:

  • let the URL be valid only for a specific time period
  • allow transfers only from a specific IP address associated with the unique URL
  • let your server-side code detect when the content for a unique URL has been fully transferred and then invalidate the URL.

Let me clarify the last bullet. Say you're using Java - you will in.read(buffer) and out.write(buffer) in a loop until EOF. If the client disconnects you will receive an IOException during out.write() and will be able to tell a successful download from an interrupted one. On other platforms, I'm sure there are ways to tell whether the connection was lost or not.

EDIT: You could actually fire a browser event using the trick outlined in the accepted answer of one of the questions you linked to. That would however not be a reliable solution to limit the number of downloads.

Detect user download

How about using Google analytics like mentioned in below post?

Best way to count number of downloads of several files on website

detecting when the File download popup is closed

I had to work on this kind of issue, on another project. I finally found a smart solution, as explained in another Stackoverflow question.

The explanation is given in the following post: http://gruffcode.com/2010/10/28/detecting-the-file-download-dialog-in-the-browser

The idea is to "simply" use a cookie to define when the file is downloaded.

Large file downloads don't work in Firefox browser

I solved this by installing FileSaver.js. It's used for large files purposes.

npm install file-saver --save


Related Topics



Leave a reply



Submit