Saturday, June 30, 2012

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();
   }

No comments:

Post a Comment