1

I would like to trigger an action when Apache detects that a certain file URL has been started for download (or: successfully downloaded).

Example: when https://example.com/download/token_A6FZ523/myfile.zip is downloaded by a client, execute the following query to a SQLite database:

INSERT INTO downloads(date, tokenID) VALUES(CURRENT_TIMESTAMP, "A6FZ523");

Usage: then, in a PHP Dashboard, I can check who has downloaded the delivered files.

I could do this by:

  • running a script every minute on the server,
  • parsing the Apache logs /var/log/apache2/other_vhosts_access.log in search for a pattern download/token_.*/myfile.zip

  • execute the INSERT INTO query in this case

This seems rather complex and the fact of having to run the script every minute is not a nice solution.

What is a good solution to ask Apache to save to a SQLite database the information "The file associated to download token A6FZ523 has been downloaded by the client."? Or maybe should PHP be used instead?

Basj
  • 41,386
  • 99
  • 383
  • 673
  • What does "downloaded" mean? How would you ever know they didn't cancel the download? – Mike 'Pomax' Kamermans Apr 26 '19 at 21:30
  • @Mike'Pomax'Kamermans The information "The user started the download" is enough for my purpose (I don't really need to know if it succeeded or not, for now). – Basj Apr 26 '19 at 21:31
  • Congratulations: you answered your own question. When the user starts a download on your page by clicking the download link, send an analytics events to whatever service (your own, or third party) you're using, either client side by adding a click listener to the download link, or server-side by the server registering that a file URL just got hit. – Mike 'Pomax' Kamermans Apr 26 '19 at 21:32
  • @Mike'Pomax'Kamermans The download link is often in an email (it could even be a plain text email, no HTML). I can't add a Javascript Event Listener in an email to detect when the link has been clicked (even if I could, it would not be an elegant solution). I would really like to detect when the file has been started being downloaded directly on server. How to do this? – Basj Apr 26 '19 at 21:41

2 Answers2

3

I think your problem lies in that you are directly fetching a file that is stored on the server, as opposed to using PHP to "serve" this file programatically. This isn't the first problem you will encounter with this method, you also can't check for security or get the file from external file storage (generally speaking, you don't store files directly on the web server these days!).

But, simple to do once you know how :)

Firstly, lets change the URL you download your file from to something like https://example.com/download.php?token=A6FZ523

So, we are sending a GET variable to a php script named "download.php". In that script you will have something like the following:

<?php 
$token = $_GET['token'];

// Get the information about the file from the DB, something like:
// SELECT filename, size, path FROM files WHERE token = $token;
// Giving you $filename, $size and $path

header('Content-Description: File Transfer');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename=' . $filename); 
header('Content-Transfer-Encoding: binary');
header('Connection: Keep-Alive');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . $size);

echo file_get_contents($path);

// This will be on a completed download
// INSERT INTO downloads(date, tokenID) VALUES(CURRENT_TIMESTAMP, $token);
?>

When the download.php file is called, the token is taken and matched to a file's info in the DB. You then set headers which basically tells the browser "this is a file", your browser responds accordingly by implementing a file download as normal. You then read the contents of the file to the user. Once this has been completed, you can log the download via another DB call.

A big thing to say is that this script (obviously with the DB calls written in) should do the very basics for you, but there is a lot more to add depending on your usage scenario. Think security, input validation, where you store your files and sending a MIME type header.

Hopefully that should point you in the right direction though :)

Jamie Robinson
  • 832
  • 10
  • 16
  • 1
    Thank you for your answer. Two little things: 1) If I still want to give `https://example.com/download/token_A6FZ523/myfile.zip` to the client, instead of `/download.php?token=A6FZ523`, which kind of Apache URL rewriting rule would you use? 2) I am happy to copy/paste all these headers, but would you have a source/doc showing which ones of these headers are really important/mandatory, which ones are optional, etc. in order to be sure to not forget anything. – Basj Apr 26 '19 at 21:56
  • 1
    Honestly, I would if at all possible change the download URL! I'm sure it's possible with an Apache rewrite rule, but I'm not the expert on them (/hate them!) so probably best to start another question on that. Http headers (being what those are) conform to html/browser standards; they aren't a PHP thing. For instance, see [this](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) for docs on the cache control header. I know this set of headers makes all the browsers I've tested respond correctly is my honest answer... – Jamie Robinson Apr 26 '19 at 22:05
  • 1
    This is a helpful answer. As some of the other answers have said, be sure to complete server-side authentication of the file request. Don't give the client access to directories or any server side authentication data. Using a token is a good way to do this, but you'll want to be sure to do user authentication and confirm privileges prior to serving the file. – FontFamily Sep 22 '21 at 00:26
1

If you have access to server and authority to install thins you could add mod_log_sql and have the apache save the log directly into a database table (it even parse the info for you) them in your dashboard you can just do simple queries. The "thing" here it seams that you are in need to get the name of the downloader, therefore you should add that "tokenID" to your URL and set the Apache to deny the url if tokenID is not present. You would need to parse the tokenID from url in the log thought.

RicardoPHP
  • 492
  • 3
  • 10
  • Thank you for your answer! Could you maybe edit to include some code example to make easier to test? Thank you in advance. – Basj Apr 27 '19 at 13:01
  • have a look in this link https://colekcolek.com/2012/03/12/save-apache-access-log-mysql-database-libaapche2-mod-log-sql-debian-squeeze/ – RicardoPHP May 29 '19 at 16:38