1

I'm a .NET guy attempting a PHP thing here, so am totally out of my comfort zone right now. What I THINK I want to do is to have 3 files:

  • download.php:

    (a) contains a lookup of IDs to filenames (so download.php?file=11 querystring tells me I should host abc.zip)

    (b) Some code to log this download to stats.log

    (c) A couple header() calls and a readfile() call, similar to the answer to this question

  • stats.log: A simple log file that might look like the following example. This allows for logging to be accomplished by simply appending a line of text yet allows me to condense it from time to time.


abc.zip 1234
xyz.zip 4321
abc.zip 1
abc.zip 1
abc.zip 1
xyz.zip 1
abc.zip 1

  • stats.php: This is ultimately the PHP file that serves the stats. They can be real-time or near real-time, perhaps re-reading the file every minute and caching it or whatever. I don't really care and this won't be hit all that often but I do need to make sure that this isn't a stupidly expensive operation. This need not be a pretty page. Something so a human can easily read it is all that matters, so no fancy requirements there. For the above example of stats.log, I'd like this to serve something like the following:

abc.zip: 1238 downloads
xyz.zip: 4322 downloads

Ultimately, I don't want a database or any other systems involved in this. I only have FTP access to the server, so I can't really do much other than place scripts into the directory. I realize that I'll need to make sure that the script has write permissions to stats.txt, which is fine.

So my questions. I have a number of them but I believe they're all quite easy for somebody who knows PHP.

  1. I think I have the hosting portion of download.php understood by setting headers and using readfile. However, how could I have a collection of key/value pairs representing file ids and filenames? If I were in .NET, I could do something like: var foo = new Dictionary<int, string> {{11, "abc.zip"}, {12, "xyz.zip"}} but I don't have a clue what this looks like in PHP.
  2. How do I get querystrings? I need to pull from the URL "stuff/download.php?file=11" and take the 11 to grab my "abc.zip" out of my lookup collection.
  3. How do I write the newline to my stats.log file?
  4. How do I loop through my stats.log file in my stats.php script to count up and host these stats?
  5. Bonus question: How do I cache the results from step 4 and only read the file once every minute/hour/or whatever?

I can probably fill in some gaps if somebody can answer at least most of these questions, but help sure would be appreciated! :)

Community
  • 1
  • 1
Jaxidian
  • 13,081
  • 8
  • 83
  • 125

2 Answers2

1

1- You are looking for array e.g.

 $files=array(11=>'abc.zip',
             12=>'xyz.zip');

2- The Query String is accessed by the super global $_GET, so in your case $_GET['file'] holds that data you are interested in.

3,4,5 I would recommend storing the information JSON encoded. e.g.

$rawInfo=file_get_contents('stats.log');
$Info=json_decode($rawInfo,true);
if(isset($Info[$_GET['file']])){
    $Info[$_GET['file']]++;
}else{
    $Info[$_GET['file']]=1;
}
$rawInfo=json_encode($Info);
$h=fopen('stats.log','c');// If $h is false, you couldn't open the file
flock($h,LOCK_EX);// lock the file
$b=fwrite($h,$rawInfo);// if $b is not greater than 0, nothing was written
flock($h,LOCK_UN);
fclose($h);
//And then actually serve the file requested

This has the advantage of storing the information already in a useful format.

Whenever you fetch out the json_decodeed data, it is in the format of an array, which you will need to know how to handle.

stats.php might look something like this:

$rStats=file_get_contents('stats.log');
$Stats=json_decode($rStats,true);
foreach($Stats as $k=>$v){
   echo $k.': '.$v.' download'.($v==1?'':'s');
}
Shad
  • 15,134
  • 2
  • 22
  • 34
  • Follow-up question: How does this handle simultaneous downloads? Will I be losing download counts or locking the stats file therefore causing errors in other downloads, or anything like that? I figured appending a line would handle concurrency concerns much better than rewriting the entire file. Interesting idea storing it as JSON data - I didn't think of that. – Jaxidian Aug 10 '11 at 02:55
  • Okay, I think I've answered my own question. I need to lock the file before opening it and writing to it, then release the lock, similar to the answer to this question: http://stackoverflow.com/questions/293601/php-and-concurrent-file-access – Jaxidian Aug 10 '11 at 03:05
  • @Jaxidian Haha, we came up with the subsequent answer at the same time, see my edited response. – Shad Aug 10 '11 at 03:07
  • Thanks! I think this fills in all of the gaps that I was obviously missing. I still have some learning and hacking to do with this script, but I think you've given me everything I need to succeed now. Thank you! – Jaxidian Aug 10 '11 at 03:10
  • @Jaxidian No problem =), I added the stats.php to my answer after I saw what you wanted it to look like. – Shad Aug 10 '11 at 03:14
0

I could have never done this so easily without @Shad's help in the accepted answer. As such, I wanted to post my final solution here that should work for practically anybody. This allows "direct links" (i.e. no 301/302 or other kinds of redirects) to function properly (right-click -> save as works too) while still logging downloads. NOTE that this is fairly "resource heavy" and some shared hosts may get upset with using something like this but as far as I can tell, this shouldn't really be a major drain. My files I'll be hosting are ~3-15MB and won't have a TON of downloads, so I'm not too worried about this in my scenario but if you use this solution, be very aware of this fact!

download.php:

<?php
$fileLookup = array(
            0=>'subdir/test.zip',
            1=>'another/sub/dir/test2.zip'
        );

$currentRelativeFileName = $fileLookup[$_GET['file']];
$currentFileName = basename($currentRelativeFileName);

$rawInfo=file_get_contents('stats.log');
$Info=json_decode($rawInfo,true);

if(isset($Info[$currentFileName])){
    $Info[$currentFileName]++;
}else{
    $Info[$currentFileName]=1;
}
$rawInfo=json_encode($Info);
$h=fopen('stats.log','c');// If $h is false, you couldn't open the file
flock($h,LOCK_EX);// lock the file
$b=fwrite($h,$rawInfo);// if $b is not greater than 0, nothing was written
flock($h,LOCK_UN);
fclose($h);

header("Content-type: application/octet-stream"); 
header("Content-disposition: attachment; filename=" . $currentFileName); 
readfile($currentRelativeFileName);

?>

stats.php:

<html>
<head>
    <title>Here are my stats!</title>
</head>
<body>
<?php
$rStats=file_get_contents('stats.log');
if (strlen($rStats) > 5){
    $Stats=json_decode($rStats,true);
    foreach($Stats as $k=>$v){
       echo $k.': '.$v.' download'.($v==1?'':'s') . '<br />';
    }
}else{
    echo 'No downloads';
}
?>
</body>
Jaxidian
  • 13,081
  • 8
  • 83
  • 125