Saturday, November 3, 2012

AJAX File Download with PHP


You would think this would be reasonably easy, but it isn’t.  It might be possible to do it with a raw XMLHttpRequest, and then again, it might not be.  The problem we have is that we want the browser to interpret it as a new page.  I initially attempted to do this with something like the following using JQuery:

$('#target-iframe).load('page.ajax' + requestParams');

where “target-iframe” is, of course, the contents of an iframe.  But I pretty quickly concluded it was never going to work.

However, I came up with an almost equally simple method that works very well.  It’s not really AJAX per se, but the difference is imperceptible to the end user.  The following short Javascript snippet will immediately start a download, assuming of course that the target URL returns the expected download headers.

function initiateFileDownload() {
   var requestParams = "/f_download/1";
   requestParams += getPageFilters('', '');
   $("#target-iframe").attr('src','page.ajax' + requestParams);
}

<iframe id="target-iframe" name="target-iframe" src="#" style="width:600;height:100;border:0px solid #fff;display:none"></iframe>

In this case, I absolutely had to do an Ajax type loader because I was trying to download results from a page that the user had already filtered down.  I could have replicated the filter code and regenerated the page, but then I have to worry about keeping the filter logic in synch between my downloader and the actual page, which is a scenario best avoided.

Here is my PHP download code that would sit somewhere such that it gets called by “page.ajax”.

ob_clean();
header('Content-Description: File Transfer');
header('Content-Type: application/force-download');
header('Content-Disposition: attachment; filename="'.$filename.'";');
header('Content-Transfer-Encoding: binary');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Pragma: public');
header('Content-Length: ' . strlen($data));
echo $data;
ob_flush();
flush();

Saturday, October 13, 2012

Semaphore class for PHP



PHP has built in support for semaphores via the sem_get(), sem_acquire(), and sem_release() commands. But support for semaphores must be compiled into PHP, and it is not enabled by default.  So it sometimes happens that you are trying to develop for a system where it has not been included, and you cannot just recompile the PHP (like in the case you are trying to develop for company X’s production servers).  In a case like this if you need semaphores, you have no choice but to roll your own.

Note the “maxLockMinutes” parameter in the “getSemaphore()” method.  If something happens and you fail to clean up at the end of this method, you will not end up locking yourself out of the resource permanently.  Once the process that has created the semaphore terminates, the lock on the file will be released.  But if the process hangs or otherwise lingers around, you need some way to break the lock.  The “maxLockMinutes=5” parameter tells the semaphore class to ignore any file lock that is older than X minutes.  I have defaulted it to 5 minutes here but you’ll want to tune it for your own system. 

The bad thing about this is that the calling program that knows how long it needs to lock the resource for cannot dictate it.  We could embed the lock time in the filename or possibly write it to the file if that is a requirement. 

I left the Semaphore class embedded in the page I used to test it.


<html>

<body>

<?php

define("UNIQUE_KEY", '123456');
define("BASE_DIRECTORY", '/tmp/');


class Semaphore {

   /* var stream */
   private $fp;

   /* var string */
   private $filename;

   /**
    * Constructor
    */
   public function __construct() {
   }

   /**
·       Attempt to get an exclusive lock on this semaphore
·        
    * @param string $baseDirectory
    * @param string $uniqueKey
    * @param int $maxLockMinutes
    *
    * @return bool
    */
   public function getSemaphore($baseDirectory, $uniqueKey, $maxLockMinutes=5) {
      $this->filename = $baseDirectory . 'semlock-' . $uniqueKey . '.sem';
      $this->releaseExpiredSemaphore($maxLockMinutes);
      $this->fp = fopen($this->filename, 'w');
      return (flock($this->fp, LOCK_EX | LOCK_NB));
   }

   /**
·       Release expired semaphore
    *
    * @param int $maxValidLockMinutes
    */
   private function releaseExpiredSemaphore(($maxValidLockMinutes) {
      if (file_exists($this->filename)) {
         $fileDate = filemtime($this->filename);
         if (time() - $fileDate > ($maxValidLockMinutes  * 60) {
            unlink($this->filename);
         }
      }
   }

   /**
    * Release semaphore
    */
   public function releaseSemaphore() {
      flock($this->fp, LOCK_UN);
      fclose($this->fp);
      unlink($this->filename);
   }
}

$semTest = new Semaphore();

$sem = $semTest->getSemaphore(BASE_DIRECTORY, UNIQUE_KEY, 5);
if($sem) {
   echo "Got lock <br>";
} else {
   echo "Failed to get lock. Boohoo! <br />";
   die();
}
for($i = 0; $i < 60; $i++) {
   sleep(5);
   echo ("sleeping " . $i * 5 . " hold you horses...<br />");
}
$semTest->releaseSemaphore();

?>


</body>
</html>