Saturday, June 30, 2012

Deleting X Oldest Files in a Directory Using PHP


Sometimes, when processing uploaded files for instance, we want to save the files for posterity.  But, over a couple years, the amount of disk space this consumes can get out of hand—particularly for really large files like database backups. 

So I wrote the following PHP function to police the directory for cases like this.  This function will keep some number of files in directory “$path” determined by the “$numToKeep” argument.  It will delete any other older files. 

It gets the file name and age (creation timestamp actually, which is not quite the same as age) in two separate arrays.  It then sorts both arrays using the file timestamp array as a key leaving you a list of file names sorted by age.  But age and timestamp are inversely related.  So we have to sort by DESC timestamp to get a list sorted by ASC age.  

The “unlink” command is permanent as I am sure you are aware.   I try to step up to another level of careful anytime I create and deploy a function of this sort.

I’ve actually used and tested this function.  It seems to works correctly.  However, I of course accept no responsibility or liability if it eats your homework…or all your company’s financial data.  Test, test, and then test again before deploying something like this against a live system.

   /**
    * Delete oldest files in a directory
    * must exist or this function will throw an exception
    *
    * @param string $path
    * @param int $numToKeep
    *
    * @return bool
    */
   private function _deleteOldestFiles($path, $numToKeep) {
  
      $fileList = array();
      $fileDate = array();
      $handle = opendir($path);
      if($handle) {
         while(false !== ($file = readdir($handle))) {
            if($file != "." && $file != "..") {
               $fileList[] = $path . $file;
               $fileDate[] = filemtime($path . $file);
            }
         }
         closedir($handle);
         array_multisort ($fileDate, SORT_DESC, $fileList);
         for ($i=$numToKeep; $i < count($fileList); $i++) {
            unlink($fileList[$i]);
            $this->logger->log('[' . __METHOD__ . '] ' .
                  'Deleted file number ' . $i . ' : ' . $fileList[$i] .
                  ' with creation date ' . $fileDate[$i]);
         }
      } else {
         $this->logger->log('[' . __METHOD__ . '] ' .
             'Error: Failed to find target deletion directory at: ' . $path);
         throw new Exception("Failed to find deletion directory: " . $path);
      }
   }


Detecting Incomplete File Uploads in PHP


Detecting Incomplete File Uploads in PHP

PHP has no good mechanism for doing this.  The best solution I have been able to come up with is to check the file size, wait some amount of time, and check it again.  If I were processing an upload directory for instance, I would sample the file size as I was building the file list, save the size, and then check it again when I go to actually process the files.  But even so, you likely need to add a sleep command in there somewhere.  The resulting code looks something like this:

   const FILE_DETECTION_DELAY = 5;
  
   /**
    * Gets a list of files in a given directory.  The directory
    * must exist or this function will throw an exception
    *
    * @param string $path
    *
    * @return array
    */
   private function _getFilesInDirectory($path) {
  
      $fileList = array();
      $fileSize = array();
      $handle = opendir($path);
      if($handle) {
         while(false !== ($file = readdir($handle))) {
            if($file != "." && $file != "..") {
               $fileName[] = $path . $file;
               $fileSize[] = filesize($path . $file);
            }
         }
         closedir($handle);
      } else {
         $this->logger->log('[' . __METHOD__ . '] ' .
                      'Error: Failed to find directory at: ' . $path);
         throw new Exception("Failed to find directory: " . $path);
      }
      sleep (self::FILE_DETECTION_DELAY);
      $fileList = array();
      for ($i=0;$i<count($fileName);$i++) {
         if (filesize($fileName[$i]) == $fileSize[$i]) {
            $fileList[] = $fileName[$i];
         }
      }
      return $fileList;
   }


This is just a quick attempt at creating this code.  I’ve never actually run this function.  So it’s possible there may be in an error in it. 

The reason I don’t use this function is there’s still a possibility that the file may be under upload, but the upload has temporarily stalled.  Longer delays reduce the risk of this, but they slow down code execution.   You can trade off portability for a more definitive solution to this problem (providing you are running under Unix or Linux) by using the “lsof” command.  “lsof” stands for “list open files”.  Run with no arguments, it will do exactly that.  But, if you run it with a specific file as an argument, it will return a list of processes using that file (like an FTP upload for instance).  If no process is using the file, it returns nothing.  Therefore, the following simple function can definitively detect an open file on Unix flavored systems.

   /**
    * REQUIRES: Unix OS
    * Checks for an open file with the UNIX "lsof" command
    * If the file is open, this command will return process
    * information for the process using it.
    * If the file is not open, this command returns nothing.
    * This makes the code dependent on a Unix system supporting the "lsof" command
    *
    * @param string $file
    * @return bool
    */
   private function _fileIsOpen($file) {
      $status = system('lsof ' . $file);
      if($status) {
         return true;
      } else {
         return false;
      }
   }

So if we wanted to produce something similar to what we have in the first example, we’d simply add a function to find all the files:

   /**
    * Gets a list of files in a given directory.  The directory
    * must exist or this function will throw an exception
    *
    * @param string $path
    *
    * @return bool
    */
   private function _getFilesInDirectory($path) {
  
      $fileList = array();
      $handle = opendir($path);
      if($handle) {
         while(false !== ($file = readdir($handle))) {
            if($file != "." && $file != "..") {
               $fileList[] = $path . $file;
            }
         }
         closedir($handle);
         return $fileList;
      } else {
         $this->logger->log('[' . __METHOD__ . '] ' .
               'Error: Failed to find directory at: ' . $path);
         throw new Exception("Failed to find directory: " . $path);
      }
   }

And finally, some kind of overall controller to call these:

   /**
    * A function to retrieve all uploaded files not still
    * in the process of being uploaded.
    *
    * @param string $path
    *
    * @return array
    */
   private function _getCompletedUploadList($path) {
      $fileList = $this->_getFilesInDirectory($path);
      $validFiles = array();
      foreach ($fileList as $file) {
         if (!$this->_fileIsOpen($file)) {
            $validFiles[] = $file;
         }
      }
      return $validFiles();
   }

Sunday, June 17, 2012

Create a Stand-Alone Magento Web Page


Sometimes, it’s nice to just quickly whip out a single page Magento script to test something such as new API you are trying to talk to.  But, if that page is going to use Magento’s functionality, it needs to load up all the XML control files first so Magento knows where to find its classes. 

The following script does just that:

<?php

/*
 * place this in a subdirectory under docroot
 * i.e. docroot/script/testClass.php
 * http://your-test-server/script/testClass.php
 *
 */

$baseDir = dirname(dirname(__FILE__));

include_once $baseDir . "/app/Mage.php";

class testClass {

   public function run() {
      Mage::App();
      $config = Mage::GetConfig();
      $config->init();

      // your test code here 
     
   }
}

$testClass = new testClass;
$testClass->run();

?>