eZ Publish  [trunk]
mysql.php
Go to the documentation of this file.
00001 <?php
00002 /**
00003  * File containing the eZDFSFileHandlerMySQLBackend class.
00004  *
00005  * @copyright Copyright (C) 1999-2012 eZ Systems AS. All rights reserved.
00006  * @license http://www.gnu.org/licenses/gpl-2.0.txt GNU General Public License v2
00007  * @version //autogentag//
00008  * @package kernel
00009  */
00010 
00011 /*
00012 This is the structure / SQL CREATE for the DFS database table.
00013 It can be created anywhere, in the same database on the same server, or on a
00014 distinct database / server.
00015 
00016 CREATE TABLE ezdfsfile (
00017   `name` text NOT NULL,
00018   name_trunk text NOT NULL,
00019   name_hash varchar(34) NOT NULL DEFAULT '',
00020   datatype varchar(255) NOT NULL DEFAULT 'application/octet-stream',
00021   scope varchar(25) NOT NULL DEFAULT '',
00022   size bigint(20) unsigned NOT NULL DEFAULT '0',
00023   mtime int(11) NOT NULL DEFAULT '0',
00024   expired tinyint(1) NOT NULL DEFAULT '0',
00025   `status` tinyint(1) NOT NULL DEFAULT '0',
00026   PRIMARY KEY (name_hash),
00027   KEY ezdfsfile_name (`name`(250)),
00028   KEY ezdfsfile_name_trunk (name_trunk(250)),
00029   KEY ezdfsfile_mtime (mtime),
00030   KEY ezdfsfile_expired_name (expired,`name`(250))
00031 ) ENGINE=InnoDB;
00032  */
00033 
00034 class eZDFSFileHandlerMySQLBackend
00035 {
00036     /**
00037      * Connects to the database.
00038      *
00039      * @return void
00040      * @throw eZClusterHandlerDBNoConnectionException
00041      * @throw eZClusterHandlerDBNoDatabaseException
00042      */
00043     public function _connect()
00044     {
00045         $siteINI = eZINI::instance( 'site.ini' );
00046         // DB Connection setup
00047         // This part is not actually required since _connect will only be called
00048         // once, but it is useful to run the unit tests. So be it.
00049         // @todo refactor this using eZINI::setVariable in unit tests
00050         if ( self::$dbparams === null )
00051         {
00052             $fileINI = eZINI::instance( 'file.ini' );
00053 
00054             self::$dbparams = array();
00055             self::$dbparams['host']       = $fileINI->variable( 'eZDFSClusteringSettings', 'DBHost' );
00056             self::$dbparams['port']       = $fileINI->variable( 'eZDFSClusteringSettings', 'DBPort' );
00057             self::$dbparams['socket']     = $fileINI->variable( 'eZDFSClusteringSettings', 'DBSocket' );
00058             self::$dbparams['dbname']     = $fileINI->variable( 'eZDFSClusteringSettings', 'DBName' );
00059             self::$dbparams['user']       = $fileINI->variable( 'eZDFSClusteringSettings', 'DBUser' );
00060             self::$dbparams['pass']       = $fileINI->variable( 'eZDFSClusteringSettings', 'DBPassword' );
00061 
00062             self::$dbparams['max_connect_tries'] = $fileINI->variable( 'eZDFSClusteringSettings', 'DBConnectRetries' );
00063             self::$dbparams['max_execute_tries'] = $fileINI->variable( 'eZDFSClusteringSettings', 'DBExecuteRetries' );
00064 
00065             self::$dbparams['sql_output'] = $siteINI->variable( "DatabaseSettings", "SQLOutput" ) == "enabled";
00066 
00067             self::$dbparams['cache_generation_timeout'] = $siteINI->variable( "ContentSettings", "CacheGenerationTimeout" );
00068         }
00069 
00070         $serverString = self::$dbparams['host'];
00071         if ( self::$dbparams['socket'] )
00072             $serverString .= ':' . self::$dbparams['socket'];
00073         elseif ( self::$dbparams['port'] )
00074             $serverString .= ':' . self::$dbparams['port'];
00075 
00076         $maxTries = self::$dbparams['max_connect_tries'];
00077         $tries = 0;
00078         eZDebug::accumulatorStart( 'mysql_cluster_connect', 'MySQL Cluster', 'Cluster database connection' );
00079         while ( $tries < $maxTries )
00080         {
00081             if ( $this->db = mysql_connect( $serverString, self::$dbparams['user'], self::$dbparams['pass'] ) )
00082                 break;
00083             ++$tries;
00084         }
00085         eZDebug::accumulatorStop( 'mysql_cluster_connect' );
00086         if ( !$this->db )
00087             throw new eZClusterHandlerDBNoConnectionException( $serverString, self::$dbparams['user'], self::$dbparams['pass'] );
00088 
00089         if ( !mysql_select_db( self::$dbparams['dbname'], $this->db ) )
00090             throw new eZClusterHandlerDBNoDatabaseException( self::$dbparams['dbname'] );
00091 
00092         // DFS setup
00093         if ( $this->dfsbackend === null )
00094         {
00095             $this->dfsbackend = new eZDFSFileHandlerDFSBackend();
00096         }
00097 
00098         $charset = trim( $siteINI->variable( 'DatabaseSettings', 'Charset' ) );
00099         if ( $charset === '' )
00100         {
00101             $charset = eZTextCodec::internalCharset();
00102         }
00103 
00104         if ( $charset )
00105         {
00106             if ( !mysql_query( "SET NAMES '" . eZMySQLCharset::mapTo( $charset ) . "'", $this->db ) )
00107             {
00108                 $this->_fail( "Failed to set Database charset to $charset." );
00109             }
00110         }
00111     }
00112 
00113     /**
00114      * Disconnects the handler from the database
00115      */
00116     public function _disconnect()
00117     {
00118         if ( $this->db !== null )
00119         {
00120             mysql_close( $this->db );
00121             $this->db = null;
00122         }
00123     }
00124 
00125     /**
00126      * Creates a copy of a file in DB+DFS
00127      * @param string $srcFilePath Source file
00128      * @param string $dstFilePath Destination file
00129      * @param string $fname
00130      * @return bool
00131      *
00132      * @see _copyInner
00133      */
00134     public function _copy( $srcFilePath, $dstFilePath, $fname = false )
00135     {
00136         if ( $fname )
00137             $fname .= "::_copy($srcFilePath, $dstFilePath)";
00138         else
00139             $fname = "_copy($srcFilePath, $dstFilePath)";
00140 
00141         // fetch source file metadata
00142         $metaData = $this->_fetchMetadata( $srcFilePath, $fname );
00143         // if source file does not exist then do nothing.
00144         // @todo Throw an exception here.
00145         //       Info: $srcFilePath
00146         if ( !$metaData )
00147         {
00148             return false;
00149         }
00150         return $this->_protect( array( $this, "_copyInner" ), $fname,
00151                                 $srcFilePath, $dstFilePath, $fname, $metaData );
00152     }
00153 
00154     /**
00155      * Inner function used by _copy to perform the operation in a transaction
00156      *
00157      * @param string $srcFilePath
00158      * @param string $dstFilePath
00159      * @param bool   $fname
00160      * @param array  $metaData Source file's metadata
00161      * @return bool
00162      *
00163      * @see _copy
00164      */
00165     private function _copyInner( $srcFilePath, $dstFilePath, $fname, $metaData )
00166     {
00167         $this->_delete( $dstFilePath, true, $fname );
00168 
00169         $datatype        = $metaData['datatype'];
00170         $filePathHash    = md5( $dstFilePath );
00171         $scope           = $metaData['scope'];
00172         $contentLength   = $metaData['size'];
00173         $fileMTime       = $metaData['mtime'];
00174         $nameTrunk       = self::nameTrunk( $dstFilePath, $scope );
00175 
00176         // Copy file metadata.
00177         if ( $this->_insertUpdate( self::TABLE_METADATA,
00178                                    array( 'datatype'=> $datatype,
00179                                           'name' => $dstFilePath,
00180                                           'name_trunk' => $nameTrunk,
00181                                           'name_hash' => $filePathHash,
00182                                           'scope' => $scope,
00183                                           'size' => $contentLength,
00184                                           'mtime' => $fileMTime,
00185                                           'expired' => ($fileMTime < 0) ? 1 : 0 ),
00186                                    "datatype=VALUES(datatype), scope=VALUES(scope), size=VALUES(size), mtime=VALUES(mtime), expired=VALUES(expired)",
00187                                    $fname ) === false )
00188         {
00189             return $this->_fail( $srcFilePath, "Failed to insert file metadata on copying." );
00190         }
00191 
00192         // Copy file data.
00193         if ( !$this->dfsbackend->copyFromDFSToDFS( $srcFilePath, $dstFilePath ) )
00194         {
00195             return $this->_fail( $srcFilePath, "Failed to copy DFS://$srcFilePath to DFS://$dstFilePath" );
00196         }
00197         return true;
00198     }
00199 
00200     /**
00201      * Purges meta-data and file-data for a file entry
00202      *
00203      * Will only expire a single file. Use _purgeByLike to purge multiple files
00204      *
00205      * @param string $filePath Path of the file to purge
00206      * @param bool $onlyExpired Only purges expired files
00207      * @param bool|int $expiry
00208      * @param bool $fname
00209      *
00210      * @see _purgeByLike
00211      */
00212     public function _purge( $filePath, $onlyExpired = false, $expiry = false, $fname = false )
00213     {
00214         if ( $fname )
00215             $fname .= "::_purge($filePath)";
00216         else
00217             $fname = "_purge($filePath)";
00218         $sql = "DELETE FROM " . self::TABLE_METADATA . " WHERE name_hash=" . $this->_md5( $filePath );
00219         if ( $expiry !== false )
00220         {
00221             $sql .= " AND mtime<" . (int)$expiry;
00222         }
00223         elseif ( $onlyExpired )
00224         {
00225             $sql .= " AND expired=1";
00226         }
00227         if ( !$this->_query( $sql, $fname ) )
00228         {
00229             return $this->_fail( "Purging file metadata for $filePath failed" );
00230         }
00231         if ( mysql_affected_rows( $this->db ) == 1 )
00232         {
00233             $this->dfsbackend->delete( $filePath );
00234         }
00235         return true;
00236     }
00237 
00238     /**
00239      * Purges meta-data and file-data for files matching a pattern using a SQL
00240      * LIKE syntax.
00241      *
00242      * @param string $like
00243      *        SQL LIKE string applied to ezdfsfile.name to look for files to
00244      *        purge
00245      * @param bool $onlyExpired
00246      *        Only purge expired files (ezdfsfile.expired = 1)
00247      * @param integer $limit Maximum number of items to purge in one call
00248      * @param integer $expiry
00249      *        Timestamp used to limit deleted files: only files older than this
00250      *        date will be deleted
00251      * @param mixed $fname Optional caller name for debugging
00252      * @see _purge
00253      * @return bool|int false if it fails, number of affected rows otherwise
00254      * @todo This method should also remove the files from disk
00255      */
00256     public function _purgeByLike( $like, $onlyExpired = false, $limit = 50, $expiry = false, $fname = false )
00257     {
00258         if ( $fname )
00259             $fname .= "::_purgeByLike($like, $onlyExpired)";
00260         else
00261             $fname = "_purgeByLike($like, $onlyExpired)";
00262 
00263         // common query part used for both DELETE and SELECT
00264         $where = " WHERE name LIKE " . $this->_quote( $like, true );
00265 
00266         if ( $expiry !== false )
00267             $where .= " AND mtime < " . (int)$expiry;
00268         elseif ( $onlyExpired )
00269             $where .= " AND expired = 1";
00270 
00271         if ( $limit )
00272             $sqlLimit = " LIMIT $limit";
00273         else
00274             $sqlLimit = "";
00275 
00276         $this->_begin( $fname );
00277 
00278         // select query, in FOR UPDATE mode
00279         $selectSQL = "SELECT name FROM " . self::TABLE_METADATA .
00280                      "{$where} {$sqlLimit} FOR UPDATE";
00281         if ( !$res = $this->_query( $selectSQL, $fname ) )
00282         {
00283             $this->_rollback( $fname );
00284             return $this->_fail( "Selecting file metadata by like statement $like failed" );
00285         }
00286         $resultCount = mysql_num_rows( $res );
00287 
00288         // if there are no results, we can just return 0 and stop right here
00289         if ( $resultCount == 0 )
00290         {
00291             $this->_rollback( $fname );
00292             return 0;
00293         }
00294         // the candidate for purge are indexed in an array
00295         else
00296         {
00297             for ( $i = 0; $i < $resultCount; $i++ )
00298             {
00299                 $row = mysql_fetch_assoc( $res );
00300                 $files[] = $row['name'];
00301             }
00302         }
00303 
00304         // delete query
00305         $deleteSQL = "DELETE FROM " . self::TABLE_METADATA . " {$where} {$sqlLimit}";
00306         if ( !$res = $this->_query( $deleteSQL, $fname ) )
00307         {
00308             $this->_rollback( $fname );
00309             return $this->_fail( "Purging file metadata by like statement $like failed" );
00310         }
00311         $deletedDBFiles = mysql_affected_rows( $this->db );
00312         $this->dfsbackend->delete( $files );
00313 
00314         $this->_commit( $fname );
00315 
00316         return $deletedDBFiles;
00317     }
00318 
00319     /**
00320      * Deletes a file from DB
00321      *
00322      * The file won't be removed from disk, _purge has to be used for this.
00323      * Only single files will be deleted, to delete multiple files,
00324      * _deleteByLike has to be used.
00325      *
00326      * @param string $filePath Path of the file to delete
00327      * @param bool $insideOfTransaction
00328      *        Wether or not a transaction is already started
00329      * @param bool|string $fname Optional caller name for debugging
00330      * @see _deleteInner
00331      * @see _deleteByLike
00332      * @return bool
00333      */
00334     public function _delete( $filePath, $insideOfTransaction = false, $fname = false )
00335     {
00336         if ( $fname )
00337             $fname .= "::_delete($filePath)";
00338         else
00339             $fname = "_delete($filePath)";
00340         // @todo Check if this is requried: _protec will already take care of
00341         //       checking if a transaction is running. But leave it like this
00342         //       for now.
00343         if ( $insideOfTransaction )
00344         {
00345             $res = $this->_deleteInner( $filePath, $fname );
00346             if ( !$res || $res instanceof eZMySQLBackendError )
00347             {
00348                 $this->_handleErrorType( $res );
00349             }
00350         }
00351         else
00352         {
00353             return $this->_protect( array( $this, '_deleteInner' ), $fname,
00354                                     $filePath, $insideOfTransaction, $fname );
00355         }
00356     }
00357 
00358     /**
00359      * Callback method used by by _delete to delete a single file
00360      *
00361      * @param string $filePath Path of the file to delete
00362      * @param string $fname Optional caller name for debugging
00363      * @return bool
00364      */
00365     protected function _deleteInner( $filePath, $fname )
00366     {
00367         if ( !$this->_query( "UPDATE " . self::TABLE_METADATA . " SET mtime=-ABS(mtime), expired=1 WHERE name_hash=" . $this->_md5( $filePath ), $fname ) )
00368             return $this->_fail( "Deleting file $filePath failed" );
00369         return true;
00370     }
00371 
00372     /**
00373      * Deletes multiple files using a SQL LIKE statement
00374      *
00375      * Use _delete if you need to delete single files
00376      *
00377      * @param string $like
00378      *        SQL LIKE condition applied to ezdfsfile.name to look for files
00379      *        to delete. Will use name_trunk if the LIKE string matches a
00380      *        filetype that supports name_trunk.
00381      * @param string $fname Optional caller name for debugging
00382      * @return bool
00383      * @see _deleteByLikeInner
00384      * @see _delete
00385      */
00386     public function _deleteByLike( $like, $fname = false )
00387     {
00388         if ( $fname )
00389             $fname .= "::_deleteByLike($like)";
00390         else
00391             $fname = "_deleteByLike($like)";
00392         return $this->_protect( array( $this, '_deleteByLikeInner' ), $fname,
00393                                 $like, $fname );
00394     }
00395 
00396     /**
00397      * Callback used by _deleteByLike to perform the deletion
00398      *
00399      * @param string $like
00400      * @param mixed $fname
00401      * @return
00402      */
00403     private function _deleteByLikeInner( $like, $fname )
00404     {
00405         $sql = "UPDATE " . self::TABLE_METADATA . " SET mtime=-ABS(mtime), expired=1\nWHERE name like ". $this->_quote( $like, true );
00406         if ( !$res = $this->_query( $sql, $fname ) )
00407         {
00408             return $this->_fail( "Failed to delete files by like: '$like'" );
00409         }
00410         return true;
00411     }
00412 
00413     /**
00414      * Deletes DB files by using a SQL regular expression applied to file names
00415      *
00416      * @param string $regex
00417      * @param mixed $fname
00418      * @return bool
00419      * @deprecated Has severe performance issues
00420      */
00421     public function _deleteByRegex( $regex, $fname = false )
00422     {
00423         if ( $fname )
00424             $fname .= "::_deleteByRegex($regex)";
00425         else
00426             $fname = "_deleteByRegex($regex)";
00427         return $this->_protect( array( $this, '_deleteByRegexInner' ), $fname,
00428                                 $regex, $fname );
00429     }
00430 
00431     /**
00432      * Callback used by _deleteByRegex to perform the deletion
00433      *
00434      * @param mixed $regex
00435      * @param mixed $fname
00436      * @return
00437      * @deprecated Has severe performances issues
00438      */
00439     public function _deleteByRegexInner( $regex, $fname )
00440     {
00441         $sql = "UPDATE " . self::TABLE_METADATA . " SET mtime=-ABS(mtime), expired=1\nWHERE name REGEXP " . $this->_quote( $regex );
00442         if ( !$res = $this->_query( $sql, $fname ) )
00443         {
00444             return $this->_fail( "Failed to delete files by regex: '$regex'" );
00445         }
00446         return true;
00447     }
00448 
00449     /**
00450      * Deletes multiple DB files by wildcard
00451      *
00452      * @param string $wildcard
00453      * @param mixed $fname
00454      * @return bool
00455      * @deprecated Has severe performance issues
00456      */
00457     public function _deleteByWildcard( $wildcard, $fname = false )
00458     {
00459         if ( $fname )
00460             $fname .= "::_deleteByWildcard($wildcard)";
00461         else
00462             $fname = "_deleteByWildcard($wildcard)";
00463         return $this->_protect( array( $this, '_deleteByWildcardInner' ), $fname,
00464                                 $wildcard, $fname );
00465     }
00466 
00467     /**
00468      * Callback used by _deleteByWildcard to perform the deletion
00469      *
00470      * @param mixed $wildcard
00471      * @param mixed $fname
00472      * @return bool
00473      * @deprecated Has severe performance issues
00474      */
00475     protected function _deleteByWildcardInner( $wildcard, $fname )
00476     {
00477         // Convert wildcard to regexp.
00478         $regex = '^' . mysql_real_escape_string( $wildcard ) . '$';
00479 
00480         $regex = str_replace( array( '.'  ),
00481                               array( '\.' ),
00482                               $regex );
00483 
00484         $regex = str_replace( array( '?', '*',  '{', '}', ',' ),
00485                               array( '.', '.*', '(', ')', '|' ),
00486                               $regex );
00487 
00488         $sql = "UPDATE " . self::TABLE_METADATA . " SET mtime=-ABS(mtime), expired=1\nWHERE name REGEXP '$regex'";
00489         if ( !$res = $this->_query( $sql, $fname ) )
00490         {
00491             return $this->_fail( "Failed to delete files by wildcard: '$wildcard'" );
00492         }
00493         return true;
00494     }
00495 
00496     public function _deleteByDirList( $dirList, $commonPath, $commonSuffix, $fname = false )
00497     {
00498         if ( $fname )
00499             $fname .= "::_deleteByDirList(" . join( ", ", $dirList ) . ", $commonPath, $commonSuffix)";
00500         else
00501             $fname = "_deleteByDirList(" . join( ", ", $dirList ) . ", $commonPath, $commonSuffix)";
00502         return $this->_protect( array( $this, '_deleteByDirListInner' ), $fname,
00503                                 $dirList, $commonPath, $commonSuffix, $fname );
00504     }
00505 
00506     protected function _deleteByDirListInner( $dirList, $commonPath, $commonSuffix, $fname )
00507     {
00508         foreach ( $dirList as $dirItem )
00509         {
00510             if ( strstr( $commonPath, '/cache/content' ) !== false or strstr( $commonPath, '/cache/template-block' ) !== false )
00511             {
00512                 $where = "WHERE name_trunk = '$commonPath/$dirItem/$commonSuffix'";
00513             }
00514             else
00515             {
00516                 $where = "WHERE name LIKE ".$this->_quote( "$commonPath/$dirItem/$commonSuffix%", true );
00517             }
00518             $sql = "UPDATE " . self::TABLE_METADATA . " SET mtime=-ABS(mtime), expired=1\n$where";
00519             if ( !$res = $this->_query( $sql, $fname ) )
00520             {
00521                 eZDebug::writeError( "Failed to delete files in dir: '$commonPath/$dirItem/$commonSuffix%'", __METHOD__ );
00522             }
00523         }
00524         return true;
00525     }
00526 
00527     public function _exists( $filePath, $fname = false, $ignoreExpiredFiles = true, $checkOnDFS = false )
00528     {
00529         if ( $fname )
00530             $fname .= "::_exists($filePath)";
00531         else
00532             $fname = "_exists($filePath)";
00533         $row = $this->_selectOneRow( "SELECT name, mtime FROM " . self::TABLE_METADATA . " WHERE name_hash=" . $this->_md5( $filePath ),
00534                                      $fname, "Failed to check file '$filePath' existance: ", true );
00535         if ( $row === false )
00536             return false;
00537 
00538         if ( $ignoreExpiredFiles )
00539             $rc = $row[1] >= 0;
00540         else
00541             $rc = true;
00542 
00543         if ( $checkOnDFS && $rc )
00544         {
00545             $rc = $this->dfsbackend->existsOnDFS( $filePath );
00546         }
00547         return $rc;
00548     }
00549 
00550     protected function __mkdir_p( $dir )
00551     {
00552         // create parent directories
00553         $dirElements = explode( '/', $dir );
00554         if ( count( $dirElements ) == 0 )
00555             return true;
00556 
00557         $result = true;
00558         $currentDir = $dirElements[0];
00559 
00560         if ( $currentDir != '' && !file_exists( $currentDir ) && !eZDir::mkdir( $currentDir, false ) )
00561             return false;
00562 
00563         for ( $i = 1; $i < count( $dirElements ); ++$i )
00564         {
00565             $dirElement = $dirElements[$i];
00566             if ( strlen( $dirElement ) == 0 )
00567                 continue;
00568 
00569             $currentDir .= '/' . $dirElement;
00570 
00571             if ( !file_exists( $currentDir ) && !eZDir::mkdir( $currentDir, false ) )
00572                 return false;
00573 
00574             $result = true;
00575         }
00576 
00577         return $result;
00578     }
00579 
00580     /**
00581      * Fetches the file $filePath from the database to its own name
00582      *
00583      * Saving $filePath locally with its original name, or $uniqueName if given
00584      *
00585      * @param string $filePath
00586      * @param string $uniqueName Alternative name to save the file to
00587      * @return string|bool the file physical path, or false if fetch failed
00588      */
00589     public function _fetch( $filePath, $uniqueName = false )
00590     {
00591         $metaData = $this->_fetchMetadata( $filePath );
00592         if ( !$metaData )
00593         {
00594             // @todo Throw an exception
00595             eZDebug::writeError( "File '$filePath' does not exist while trying to fetch.", __METHOD__ );
00596             return false;
00597         }
00598 
00599         // create temporary file
00600         if ( strrpos( $filePath, '.' ) > 0 )
00601             $tmpFilePath = substr_replace( $filePath, getmypid().'tmp', strrpos( $filePath, '.' ), 0  );
00602         else
00603             $tmpFilePath = $filePath . '.' . getmypid().'tmp';
00604         $this->__mkdir_p( dirname( $tmpFilePath ) );
00605 
00606         // copy DFS file to temporary FS path
00607         // @todo Throw an exception
00608         if ( !$this->dfsbackend->copyFromDFS( $filePath, $tmpFilePath ) )
00609         {
00610             eZDebug::writeError("Failed copying DFS://$filePath to FS://$tmpFilePath ");
00611             return false;
00612         }
00613 
00614         // Make sure all data is written correctly
00615         clearstatcache();
00616         $tmpSize = filesize( $tmpFilePath );
00617         // @todo Throw an exception
00618         if ( $tmpSize != $metaData['size'] )
00619         {
00620             eZDebug::writeError( "Size ($tmpSize) of written data for file '$tmpFilePath' does not match expected size " . $metaData['size'], __METHOD__ );
00621             return false;
00622         }
00623 
00624         if ( $uniqueName !== true )
00625         {
00626             eZFile::rename( $tmpFilePath, $filePath, false, eZFile::CLEAN_ON_FAILURE | eZFile::APPEND_DEBUG_ON_FAILURE );
00627         }
00628         else
00629         {
00630             $filePath = $tmpFilePath;
00631         }
00632 
00633         return $filePath;
00634     }
00635 
00636     public function _fetchContents( $filePath, $fname = false )
00637     {
00638         if ( $fname )
00639             $fname .= "::_fetchContents($filePath)";
00640         else
00641             $fname = "_fetchContents($filePath)";
00642         $metaData = $this->_fetchMetadata( $filePath, $fname );
00643         // @todo Throw an exception
00644         if ( !$metaData )
00645         {
00646             eZDebug::writeError( "File '$filePath' does not exist while trying to fetch its contents.", __METHOD__ );
00647             return false;
00648         }
00649 
00650         // @todo Catch an exception
00651         if ( !$contents = $this->dfsbackend->getContents( $filePath ) )
00652         {
00653             eZDebug::writeError("An error occured while reading contents of DFS://$filePath", __METHOD__ );
00654             return false;
00655         }
00656         return $contents;
00657     }
00658 
00659     /**
00660      * Fetches and returns metadata for $filePath
00661      * @return array|false file metadata, or false if the file does not exist in
00662      *                     database.
00663      */
00664     function _fetchMetadata( $filePath, $fname = false )
00665     {
00666         if ( $fname )
00667             $fname .= "::_fetchMetadata($filePath)";
00668         else
00669             $fname = "_fetchMetadata($filePath)";
00670         $sql = "SELECT * FROM " . self::TABLE_METADATA . " WHERE name_hash=" . $this->_md5( $filePath );
00671         return $this->_selectOneAssoc( $sql, $fname,
00672                                        "Failed to retrieve file metadata: $filePath",
00673                                        true );
00674     }
00675 
00676     public function _linkCopy( $srcPath, $dstPath, $fname = false )
00677     {
00678         if ( $fname )
00679             $fname .= "::_linkCopy($srcPath,$dstPath)";
00680         else
00681             $fname = "_linkCopy($srcPath,$dstPath)";
00682         return $this->_copy( $srcPath, $dstPath, $fname );
00683     }
00684 
00685     /**
00686      * Passes $filePath content through
00687      *
00688      * @param string $filePath
00689      * @param int    $offset  Byte offset to start download from
00690      * @param int    $length  Byte length to be sent
00691      *
00692      * @return bool
00693      */
00694     public function _passThrough( $filePath, $startOffset = 0, $length = false, $fname = false )
00695     {
00696         if ( $fname )
00697             $fname .= "::_passThrough($filePath)";
00698         else
00699             $fname = "_passThrough($filePath)";
00700 
00701         $metaData = $this->_fetchMetadata( $filePath, $fname );
00702         // @todo Throw an exception
00703         if ( !$metaData )
00704             return false;
00705 
00706         // @todo Catch an exception
00707         $this->dfsbackend->passthrough( $filePath, $startOffset, $length );
00708 
00709         return true;
00710     }
00711 
00712     /**
00713      * Renames $srcFilePath to $dstFilePath
00714      *
00715      * @param string $srcFilePath
00716      * @param string $dstFilePath
00717      * @return bool
00718      */
00719     public function _rename( $srcFilePath, $dstFilePath )
00720     {
00721         if ( strcmp( $srcFilePath, $dstFilePath ) == 0 )
00722             return;
00723 
00724         // fetch source file metadata
00725         $metaData = $this->_fetchMetadata( $srcFilePath );
00726         // if source file does not exist then do nothing.
00727         // @todo Throw an exception
00728         if ( !$metaData )
00729             return false;
00730 
00731         $this->_begin( __METHOD__ );
00732 
00733         $srcFilePathStr  = mysql_real_escape_string( $srcFilePath );
00734         $dstFilePathStr  = mysql_real_escape_string( $dstFilePath );
00735         $dstNameTrunkStr = mysql_real_escape_string( self::nameTrunk( $dstFilePath, $metaData['scope'] ) );
00736 
00737         // Mark entry for update to lock it
00738         $sql = "SELECT * FROM " . self::TABLE_METADATA . " WHERE name_hash=MD5('$srcFilePathStr') FOR UPDATE";
00739         if ( !$this->_query( $sql, "_rename($srcFilePath, $dstFilePath)" ) )
00740         {
00741             // @todo Throw an exception
00742             eZDebug::writeError( "Failed locking file '$srcFilePath'", __METHOD__ );
00743             $this->_rollback( __METHOD__ );
00744             return false;
00745         }
00746 
00747         if ( $this->_exists( $dstFilePath, false, false ) )
00748             $this->_purge( $dstFilePath, false );
00749 
00750         // Create a new meta-data entry for the new file to make foreign keys happy.
00751         $sql = "INSERT INTO " . self::TABLE_METADATA . " (name, name_trunk, name_hash, datatype, scope, size, mtime, expired) SELECT '$dstFilePathStr' AS name, '$dstNameTrunkStr' as name_trunk, MD5('$dstFilePathStr') AS name_hash, datatype, scope, size, mtime, expired FROM " . self::TABLE_METADATA . " WHERE name_hash=MD5('$srcFilePathStr')";
00752         if ( !$this->_query( $sql, "_rename($srcFilePath, $dstFilePath)" ) )
00753         {
00754             eZDebug::writeError( "Failed making new file entry '$dstFilePath'", __METHOD__ );
00755             $this->_rollback( __METHOD__ );
00756             // @todo Throw an exception
00757             return false;
00758         }
00759 
00760         if ( !$this->dfsbackend->copyFromDFSToDFS( $srcFilePath, $dstFilePath ) )
00761         {
00762             return $this->_fail( "Failed to copy DFS://$srcFilePath to DFS://$dstFilePath" );
00763         }
00764 
00765         // Remove old entry
00766         $sql = "DELETE FROM " . self::TABLE_METADATA . " WHERE name_hash=MD5('$srcFilePathStr')";
00767         if ( !$this->_query( $sql, "_rename($srcFilePath, $dstFilePath)" ) )
00768         {
00769             eZDebug::writeError( "Failed removing old file '$srcFilePath'", __METHOD__ );
00770             $this->_rollback( __METHOD__ );
00771             // @todo Throw an exception
00772             return false;
00773         }
00774 
00775         // delete original DFS file
00776         // @todo Catch an exception
00777         $this->dfsbackend->delete( $srcFilePath );
00778 
00779         $this->_commit( __METHOD__ );
00780 
00781         return true;
00782     }
00783 
00784     /**
00785      * Stores $filePath to cluster
00786      *
00787      * @param string $filePath
00788      * @param string $datatype
00789      * @param string $scope
00790      * @param string $fname
00791      * @return void
00792      */
00793     function _store( $filePath, $datatype, $scope, $fname = false )
00794     {
00795         if ( !is_readable( $filePath ) )
00796         {
00797             eZDebug::writeError( "Unable to store file '$filePath' since it is not readable.", __METHOD__ );
00798             return;
00799         }
00800         if ( $fname )
00801             $fname .= "::_store($filePath, $datatype, $scope)";
00802         else
00803             $fname = "_store($filePath, $datatype, $scope)";
00804 
00805         $this->_protect( array( $this, '_storeInner' ), $fname,
00806                          $filePath, $datatype, $scope, $fname );
00807     }
00808 
00809     /**
00810      * Callback function used to perform the actual file store operation
00811      * @param string $filePath
00812      * @param string $datatype
00813      * @param string $scope
00814      * @param string $fname
00815      * @see eZDFSFileHandlerMySQLBackend::_store()
00816      * @return bool
00817      */
00818     function _storeInner( $filePath, $datatype, $scope, $fname )
00819     {
00820         // Insert file metadata.
00821         clearstatcache();
00822         $fileMTime = filemtime( $filePath );
00823         $contentLength = filesize( $filePath );
00824         $filePathHash = md5( $filePath );
00825         $nameTrunk = self::nameTrunk( $filePath, $scope );
00826 
00827         if ( $this->_insertUpdate( self::TABLE_METADATA,
00828             array( 'datatype' => $datatype,
00829                    'name' => $filePath,
00830                    'name_trunk' => $nameTrunk,
00831                    'name_hash' => $filePathHash,
00832                    'scope' => $scope,
00833                    'size' => $contentLength,
00834                    'mtime' => $fileMTime,
00835                    'expired' => ( $fileMTime < 0 ) ? 1 : 0 ),
00836             "datatype=VALUES(datatype), scope=VALUES(scope), size=VALUES(size), mtime=VALUES(mtime), expired=VALUES(expired)",
00837             $fname ) === false )
00838         {
00839             return $this->_fail( "Failed to insert file metadata while storing. Possible race condition" );
00840         }
00841 
00842         // copy given $filePath to DFS
00843         if ( !$this->dfsbackend->copyToDFS( $filePath ) )
00844         {
00845             return $this->_fail( "Failed to copy FS://$filePath to DFS://$filePath" );
00846         }
00847 
00848         return true;
00849     }
00850 
00851     /**
00852      * Stores $contents as the contents of $filePath to the cluster
00853      *
00854      * @param string $filePath
00855      * @param string $contents
00856      * @param string $scope
00857      * @param string $datatype
00858      * @param int $mtime
00859      * @param string $fname
00860      * @return void
00861      */
00862     function _storeContents( $filePath, $contents, $scope, $datatype, $mtime = false, $fname = false )
00863     {
00864         if ( $fname )
00865             $fname .= "::_storeContents($filePath, ..., $scope, $datatype)";
00866         else
00867             $fname = "_storeContents($filePath, ..., $scope, $datatype)";
00868 
00869         $this->_protect( array( $this, '_storeContentsInner' ), $fname,
00870                          $filePath, $contents, $scope, $datatype, $mtime, $fname );
00871     }
00872 
00873     function _storeContentsInner( $filePath, $contents, $scope, $datatype, $curTime, $fname )
00874     {
00875         // Insert file metadata.
00876         $contentLength = strlen( $contents );
00877         $filePathHash = md5( $filePath );
00878         $nameTrunk = self::nameTrunk( $filePath, $scope );
00879         if ( $curTime === false )
00880             $curTime = time();
00881 
00882         if ( $this->_insertUpdate( self::TABLE_METADATA,
00883             array( 'datatype' => $datatype,
00884                    'name' => $filePath,
00885                    'name_trunk' => $nameTrunk,
00886                    'name_hash' => $filePathHash,
00887                    'scope' => $scope,
00888                    'size' => $contentLength,
00889                    'mtime' => $curTime,
00890                    'expired' => ( $curTime < 0 ) ? 1 : 0 ),
00891             "datatype=VALUES(datatype), name_trunk='$nameTrunk', scope=VALUES(scope), size=VALUES(size), mtime=VALUES(mtime), expired=VALUES(expired)",
00892             $fname ) === false )
00893         {
00894             return $this->_fail( "Failed to insert file metadata while storing contents. Possible race condition" );
00895         }
00896 
00897         if ( !$this->dfsbackend->createFileOnDFS( $filePath, $contents ) )
00898         {
00899             return $this->_fail( "Failed to open DFS://$filePath for writing" );
00900         }
00901 
00902         return true;
00903     }
00904 
00905     public function _getFileList( $scopes = false, $excludeScopes = false )
00906     {
00907         $query = 'SELECT name FROM ' . self::TABLE_METADATA;
00908 
00909         if ( is_array( $scopes ) && count( $scopes ) > 0 )
00910         {
00911             $query .= ' WHERE scope ';
00912             if ( $excludeScopes )
00913                 $query .= 'NOT ';
00914             $query .= "IN ('" . implode( "', '", $scopes ) . "')";
00915         }
00916 
00917         $rslt = $this->_query( $query, "_getFileList( array( " . implode( ', ', is_array( $scopes ) ? $scopes : array() ) . " ), $excludeScopes )" );
00918         if ( !$rslt )
00919         {
00920             eZDebug::writeDebug( 'Unable to get file list', __METHOD__ );
00921             // @todo Throw an exception
00922             return false;
00923         }
00924 
00925         $filePathList = array();
00926         while ( $row = mysql_fetch_row( $rslt ) )
00927             $filePathList[] = $row[0];
00928 
00929         return $filePathList;
00930     }
00931 
00932     /**
00933      * Handles a DB error, displaying it as an eZDebug error
00934      * @see eZDebug::writeError
00935      * @param string $msg Message to display
00936      * @param string $sql SQL query to display error for
00937      * @return void
00938      */
00939     protected function _die( $msg, $sql = null )
00940     {
00941         if ( $this->db )
00942         {
00943             eZDebug::writeError( $sql, "$msg" . mysql_error( $this->db ) );
00944         }
00945         else
00946         {
00947             eZDebug::writeError( $sql, "$msg: " . mysql_error() );
00948         }
00949     }
00950 
00951     /**
00952      * Performs an insert of the given items in $array.
00953      * @param string $table Name of table to execute insert on.
00954      * @param array $array Associative array with data to insert, the keys are
00955      *                     the field names and the values will be quoted
00956      *                     according to type.
00957      * @param string $fname Name of caller function (for logging purpuse)
00958      */
00959     function _insert( $table, $array, $fname )
00960     {
00961         $keys = array_keys( $array );
00962         $query = "INSERT INTO $table (" . join( ", ", $keys ) . ") VALUES (" . $this->_sqlList( $array ) . ")";
00963         $res = $this->_query( $query, $fname );
00964         if ( !$res )
00965         {
00966             // @todo Throw an exception
00967             return false;
00968         }
00969         return mysql_insert_id( $this->db );
00970     }
00971 
00972     /**
00973      * Performs an insert of the given items in $array.
00974      *
00975      * If entry specified already exists the $update SQL is executed to update
00976      * the entry instead.
00977      *
00978      * @param string $table Name of table to execute insert on.
00979      * @param array  array $array Associative array with data to insert, the keys
00980      *                     are the field names and the values will be quoted
00981      *                     according to type.
00982      * @param string $update Partial update SQL which is executed when entry
00983      *                       exists.
00984      * @param string $fname Name of caller function (for logging purpuse)
00985      */
00986     protected function _insertUpdate( $table, $array, $update, $fname, $reportError = true )
00987     {
00988         $keys = array_keys( $array );
00989         $query = "INSERT INTO $table (" . join( ", ", $keys ) . ") VALUES (" . $this->_sqlList( $array ) . ")\nON DUPLICATE KEY UPDATE $update";
00990         $res = $this->_query( $query, $fname, $reportError );
00991         if ( !$res )
00992         {
00993             // @todo Throw an exception
00994             return false;
00995         }
00996         return mysql_insert_id( $this->db );
00997     }
00998 
00999     /**
01000      * Formats a list of entries as an SQL list which is separated by commas.
01001      * Each entry in the list is quoted using _quote().
01002      *
01003      * @param array $array
01004      * @return array
01005      */
01006     protected function _sqlList( $array )
01007     {
01008         $text = "";
01009         $sep = "";
01010         foreach ( $array as $e )
01011         {
01012             $text .= $sep;
01013             $text .= $this->_quote( $e );
01014             $sep = ", ";
01015         }
01016         return $text;
01017     }
01018 
01019     /**
01020      * Runs a select query and returns one numeric indexed row from the result
01021      * If there are more than one row it will fail and exit, if 0 it returns
01022      * false.
01023      *
01024      * @param string $query
01025      * @param string $fname The function name that started the query, should
01026      *                      contain relevant arguments in the text.
01027      * @param string $error Sent to _error() in case of errors
01028      * @param bool   $debug If true it will display the fetched row in addition
01029      *                      to the SQL.
01030      * @return array|false
01031      */
01032     protected function _selectOneRow( $query, $fname, $error = false, $debug = false )
01033     {
01034         return $this->_selectOne( $query, $fname, $error, $debug, "mysql_fetch_row" );
01035     }
01036 
01037     /**
01038      * Runs a select query and returns one associative row from the result.
01039      *
01040      * If there are more than one row it will fail and exit, if 0 it returns
01041      * false.
01042      *
01043      * @param string $query
01044      * @param string $fname The function name that started the query, should
01045      *                      contain relevant arguments in the text.
01046      * @param string $error Sent to _error() in case of errors
01047      * @param bool   $debug If true it will display the fetched row in addition
01048      *                      to the SQL.
01049      * @return array|false
01050      */
01051     protected function _selectOneAssoc( $query, $fname, $error = false, $debug = false )
01052     {
01053         return $this->_selectOne( $query, $fname, $error, $debug, "mysql_fetch_assoc" );
01054     }
01055 
01056     /**
01057      * Runs a select query, applying the $fetchCall callback to one result
01058      * If there are more than one row it will fail and exit, if 0 it returns false.
01059      *
01060      * @param string $fname The function name that started the query, should
01061      *                      contain relevant arguments in the text.
01062      * @param string $error Sent to _error() in case of errors
01063      * @param bool $debug If true it will display the fetched row in addition to the SQL.
01064      * @param callback $fetchCall The callback to fetch the row.
01065      * @return mixed
01066      */
01067     protected function _selectOne( $query, $fname, $error = false, $debug = false, $fetchCall )
01068     {
01069         eZDebug::accumulatorStart( 'mysql_cluster_query', 'MySQL Cluster', 'DB queries' );
01070         $time = microtime( true );
01071 
01072         $res = mysql_query( $query, $this->db );
01073         if ( !$res )
01074         {
01075             if ( mysql_errno( $this->db ) == 1146 )
01076             {
01077                 throw new eZDFSFileHandlerTableNotFoundException(
01078                     $query, mysql_error( $this->db ) );
01079             }
01080             else
01081             {
01082                 $this->_error( $query, $fname, $error );
01083                 eZDebug::accumulatorStop( 'mysql_cluster_query' );
01084                 // @todo Throw an exception
01085                 return false;
01086             }
01087         }
01088 
01089         $nRows = mysql_num_rows( $res );
01090         if ( $nRows > 1 )
01091         {
01092             eZDebug::writeError( 'Duplicate entries found', $fname );
01093             eZDebug::accumulatorStop( 'mysql_cluster_query' );
01094             // @todo throw an exception instead. Should NOT happen.
01095         }
01096 
01097         $row = $fetchCall( $res );
01098         mysql_free_result( $res );
01099         if ( $debug )
01100             $query = "SQL for _selectOneAssoc:\n" . $query . "\n\nRESULT:\n" . var_export( $row, true );
01101 
01102         $time = microtime( true ) - $time;
01103         eZDebug::accumulatorStop( 'mysql_cluster_query' );
01104 
01105         $this->_report( $query, $fname, $time );
01106         return $row;
01107     }
01108 
01109     /**
01110      * Starts a new transaction by executing a BEGIN call.
01111      * If a transaction is already started nothing is executed.
01112      */
01113     protected function _begin( $fname = false )
01114     {
01115         if ( $fname )
01116             $fname .= "::_begin";
01117         else
01118             $fname = "_begin";
01119         $this->transactionCount++;
01120         if ( $this->transactionCount == 1 )
01121             $this->_query( "BEGIN", $fname );
01122     }
01123 
01124     /**
01125      * Stops a current transaction and commits the changes by executing a COMMIT call.
01126      * If the current transaction is a sub-transaction nothing is executed.
01127      */
01128     protected function _commit( $fname = false )
01129     {
01130         if ( $fname )
01131             $fname .= "::_commit";
01132         else
01133             $fname = "_commit";
01134         $this->transactionCount--;
01135         if ( $this->transactionCount == 0 )
01136             $this->_query( "COMMIT", $fname );
01137     }
01138 
01139     /**
01140      * Stops a current transaction and discards all changes by executing a
01141      * ROLLBACK call.
01142      * If the current transaction is a sub-transaction nothing is executed.
01143      */
01144     protected function _rollback( $fname = false )
01145     {
01146         if ( $fname )
01147             $fname .= "::_rollback";
01148         else
01149             $fname = "_rollback";
01150         $this->transactionCount--;
01151         if ( $this->transactionCount == 0 )
01152             $this->_query( "ROLLBACK", $fname );
01153     }
01154 
01155     /**
01156      * Protects a custom function with SQL queries in a database transaction.
01157      * If the function reports an error the transaction is ROLLBACKed.
01158      *
01159      * The first argument to the _protect() is the callback and the second is the
01160      * name of the function (for query reporting). The remainder of arguments are
01161      * sent to the callback.
01162      *
01163      * A return value of false from the callback is considered a failure, any
01164      * other value is returned from _protect(). For extended error handling call
01165      * _fail() and return the value.
01166      */
01167     protected function _protect()
01168     {
01169         $args = func_get_args();
01170         $callback = array_shift( $args );
01171         $fname    = array_shift( $args );
01172 
01173         $maxTries = self::$dbparams['max_execute_tries'];
01174         $tries = 0;
01175         while ( $tries < $maxTries )
01176         {
01177             $this->_begin( $fname );
01178 
01179             $result = call_user_func_array( $callback, $args );
01180 
01181             $errno = mysql_errno( $this->db );
01182             if ( $errno == 1205 || // Error: 1205 SQLSTATE: HY000 (ER_LOCK_WAIT_TIMEOUT)
01183                  $errno == 1213 )  // Error: 1213 SQLSTATE: 40001 (ER_LOCK_DEADLOCK)
01184             {
01185                 $tries++;
01186                 $this->_rollback( $fname );
01187                 continue;
01188             }
01189 
01190             // @todo replace with an exception
01191             if ( $result === false )
01192             {
01193                 $this->_rollback( $fname );
01194                 return false;
01195             }
01196             elseif ( $result instanceof eZMySQLBackendError )
01197             {
01198                 eZDebug::writeError( $result->errorValue, $result->errorText );
01199                 $this->_rollback( $fname );
01200                 return false;
01201             }
01202 
01203             break; // All is good, so break out of loop
01204         }
01205 
01206         $this->_commit( $fname );
01207         return $result;
01208     }
01209 
01210     protected function _handleErrorType( $res )
01211     {
01212         if ( $res === false )
01213         {
01214             eZDebug::writeError( "SQL failed" );
01215         }
01216         elseif ( $res instanceof eZMySQLBackendError )
01217         {
01218             eZDebug::writeError( $res->errorValue, $res->errorText );
01219         }
01220     }
01221 
01222     /**
01223      * Checks if $result is a failure type and returns true if so, false
01224      * otherwise.
01225      *
01226      * A failure is either the value false or an error object of type
01227      * eZMySQLBackendError.
01228      */
01229     protected function _isFailure( $result )
01230     {
01231         if ( $result === false || ($result instanceof eZMySQLBackendError ) )
01232         {
01233             return true;
01234         }
01235         return false;
01236     }
01237 
01238     /**
01239      * Creates an error object which can be read by some backend functions.
01240      * @param mixed $value The value which is sent to the debug system.
01241      * @param string $text The text/header for the value.
01242      */
01243     protected function _fail( $value, $text = false )
01244     {
01245         $value .= "\n" . mysql_errno( $this->db ) . ": " . mysql_error( $this->db );
01246         return new eZMySQLBackendError( $value, $text );
01247     }
01248 
01249     /**
01250      * Performs mysql query and returns mysql result.
01251      * Times the sql execution, adds accumulator timings and reports SQL to
01252      * debug.
01253      * @param string $fname The function name that started the query, should
01254      *                      contain relevant arguments in the text.
01255      */
01256     protected function _query( $query, $fname = false, $reportError = true )
01257     {
01258         eZDebug::accumulatorStart( 'mysql_cluster_query', 'MySQL Cluster', 'DB queries' );
01259         $time = microtime( true );
01260 
01261         $res = mysql_query( $query, $this->db );
01262         if ( !$res && $reportError )
01263         {
01264             $this->_error( $query, $fname );
01265         }
01266 
01267         $numRows = mysql_affected_rows( $this->db );
01268 
01269         $time = microtime( true ) - $time;
01270         eZDebug::accumulatorStop( 'mysql_cluster_query' );
01271 
01272         $this->_report( $query, $fname, $time, $numRows );
01273         return $res;
01274     }
01275 
01276     /**
01277      * Make sure that $value is escaped and qouted according to type and returned
01278      * as a string.
01279      *
01280      * @param string $value a SQL parameter to escape
01281      * @param bool $escapeUnderscoreWildcards Set to true to escape underscores as well to avoid them to act as wildcards
01282      *                                        Highly recommended for LIKE statements !
01283      * @return string a string that can safely be used in SQL queries
01284      */
01285     protected function _quote( $value, $escapeUnderscoreWildcards = false )
01286     {
01287         if ( $value === null )
01288         {
01289             return 'NULL';
01290         }
01291         elseif ( is_integer( $value ) )
01292         {
01293             return (string)$value;
01294         }
01295         else
01296         {
01297            if ( $escapeUnderscoreWildcards )
01298                 return "'" . addcslashes( mysql_real_escape_string( $value, $this->db ), "_" ) . "'";
01299            else
01300                 return "'" . mysql_real_escape_string( $value, $this->db ) . "'";
01301         }
01302     }
01303 
01304     /**
01305      * Provides the SQL calls to convert $value to MD5
01306      * The returned value can directly be put into SQLs.
01307      */
01308     protected function _md5( $value )
01309     {
01310         return "MD5('" . mysql_real_escape_string( $value ) . "')";
01311     }
01312 
01313     /**
01314      * Prints error message $error to debug system.
01315      * @param string $query The query that was attempted, will be printed if
01316      *                      $error is \c false
01317      * @param string $fname The function name that started the query, should
01318      *                      contain relevant arguments in the text.
01319      * @param string $error The error message, if this is an array the first
01320      *                      element is the value to dump and the second the error
01321      *                      header (for eZDebug::writeNotice). If this is \c
01322      *                      false a generic message is shown.
01323      */
01324     protected function _error( $query, $fname, $error = "Failed to execute SQL for function:" )
01325     {
01326         if ( $error === false )
01327         {
01328             $error = "Failed to execute SQL for function:";
01329         }
01330         else if ( is_array( $error ) )
01331         {
01332             $fname = $error[1];
01333             $error = $error[0];
01334         }
01335 
01336         eZDebug::writeError( "$error\n" . mysql_errno( $this->db ) . ': ' . mysql_error( $this->db ), $fname );
01337     }
01338 
01339     /**
01340      * Report SQL $query to debug system.
01341      *
01342      * @param string $fname The function name that started the query, should contain relevant arguments in the text.
01343      * @param int    $timeTaken Number of seconds the query + related operations took (as float).
01344      * @param int $numRows Number of affected rows.
01345      */
01346     function _report( $query, $fname, $timeTaken, $numRows = false )
01347     {
01348         if ( !self::$dbparams['sql_output'] )
01349             return;
01350 
01351         $rowText = '';
01352         if ( $numRows !== false )
01353             $rowText = "$numRows rows, ";
01354         static $numQueries = 0;
01355         if ( strlen( $fname ) == 0 )
01356             $fname = "_query";
01357         $backgroundClass = ($this->transactionCount > 0  ? "debugtransaction transactionlevel-$this->transactionCount" : "");
01358         eZDebug::writeNotice( "$query", "cluster::mysql::{$fname}[{$rowText}" . number_format( $timeTaken, 3 ) . " ms] query number per page:" . $numQueries++, $backgroundClass );
01359     }
01360 
01361     /**
01362      * Attempts to begin cache generation by creating a new file named as the
01363      * given filepath, suffixed with .generating. If the file already exists,
01364      * insertion is not performed and false is returned (means that the file
01365      * is already being generated)
01366      * @param string $filePath
01367      * @return array array with 2 indexes: 'result', containing either ok or ko,
01368      *         and another index that depends on the result:
01369      *         - if result == 'ok', the 'mtime' index contains the generating
01370      *           file's mtime
01371      *         - if result == 'ko', the 'remaining' index contains the remaining
01372      *           generation time (time until timeout) in seconds
01373      */
01374     public function _startCacheGeneration( $filePath, $generatingFilePath )
01375     {
01376         $fname = "_startCacheGeneration( {$filePath} )";
01377 
01378         $nameHash = $this->_md5( $generatingFilePath );
01379         $mtime = time();
01380 
01381         $insertData = array( 'name' => "'" . mysql_real_escape_string( $generatingFilePath ) . "'",
01382                              'name_trunk' => "'" . mysql_real_escape_string( $generatingFilePath ) . "'",
01383                              'name_hash' => $nameHash,
01384                              'scope' => "''",
01385                              'datatype' => "''",
01386                              'mtime' => $mtime,
01387                              'expired' => 0 );
01388         $query = 'INSERT INTO ' . self::TABLE_METADATA . ' ( '. implode(', ', array_keys( $insertData ) ) . ' ) ' .
01389                  "VALUES(" . implode( ', ', $insertData ) . ")";
01390 
01391         if ( !$this->_query( $query, "_startCacheGeneration( $filePath )", false ) )
01392         {
01393             $errno = mysql_errno( $this->db );
01394             if ( $errno != 1062 )
01395             {
01396                 eZDebug::writeError( "Unexpected error #$errno when trying to start cache generation on $filePath (".mysql_error().")", __METHOD__ );
01397                 eZDebug::writeDebug( $query, '$query' );
01398 
01399                 // @todo Make this an actual error, maybe an exception
01400                 return array( 'res' => 'ko' );
01401             }
01402             // error 1062 is expected, since it means duplicate key (file is being generated)
01403             else
01404             {
01405                 // generation timout check
01406                 $query = "SELECT mtime FROM " . self::TABLE_METADATA . " WHERE name_hash = {$nameHash}";
01407                 $row = $this->_selectOneRow( $query, $fname, false, false );
01408 
01409                 // file has been renamed, i.e it is no longer a .generating file
01410                 if( $row and !isset( $row[0] ) )
01411                     return array( 'result' => 'ok', 'mtime' => $mtime );
01412 
01413                 $remainingGenerationTime = $this->remainingCacheGenerationTime( $row );
01414                 if ( $remainingGenerationTime < 0 )
01415                 {
01416                     $previousMTime = $row[0];
01417 
01418                     eZDebugSetting::writeDebug( 'kernel-clustering', "$filePath generation has timedout, taking over", __METHOD__ );
01419                     $updateQuery = "UPDATE " . self::TABLE_METADATA . " SET mtime = {$mtime} WHERE name_hash = {$nameHash} AND mtime = {$previousMTime}";
01420 
01421                     // we run the query manually since the default _query won't
01422                     // report affected rows
01423                     $res = mysql_query( $updateQuery, $this->db );
01424                     if ( ( $res !== false ) and mysql_affected_rows( $this->db ) == 1 )
01425                     {
01426                         return array( 'result' => 'ok', 'mtime' => $mtime );
01427                     }
01428                     else
01429                     {
01430                         // @todo This would require an actual error handling
01431                         eZDebug::writeError( "An error occured taking over timedout generating cache file $generatingFilePath (".mysql_error().")", __METHOD__ );
01432                         return array( 'result' => 'error' );
01433                     }
01434                 }
01435                 else
01436                 {
01437                     return array( 'result' => 'ko', 'remaining' => $remainingGenerationTime );
01438                 }
01439             }
01440         }
01441         else
01442         {
01443             return array( 'result' => 'ok', 'mtime' => $mtime );
01444         }
01445     }
01446 
01447     /**
01448      * Ends the cache generation for the current file: moves the (meta)data for
01449      * the .generating file to the actual file, and removed the .generating
01450      * @param string $filePath
01451      * @return bool
01452      */
01453     public function _endCacheGeneration( $filePath, $generatingFilePath, $rename )
01454     {
01455         $fname = "_endCacheGeneration( $filePath )";
01456 
01457         // no rename: the .generating entry is just deleted
01458         if ( $rename === false )
01459         {
01460             $this->_query( "DELETE FROM " . self::TABLE_METADATA . " WHERE name_hash=MD5('$generatingFilePath')", $fname, true );
01461             $this->dfsbackend->delete( $generatingFilePath );
01462             return true;
01463         }
01464         // rename mode: the generating file and its contents are renamed to the
01465         // final name
01466         else
01467         {
01468             $this->_begin( $fname );
01469 
01470             // both files are locked for update
01471             if ( !$res = $this->_query( "SELECT * FROM " . self::TABLE_METADATA . " WHERE name_hash=MD5('$generatingFilePath') FOR UPDATE", $fname, true ) )
01472             {
01473                 $this->_rollback( $fname );
01474                 // @todo Throw an exception
01475                 return false;
01476             }
01477             $generatingMetaData = mysql_fetch_assoc( $res );
01478 
01479             // the original file does not exist: we move the generating file
01480             $res = $this->_query( "SELECT * FROM " . self::TABLE_METADATA . " WHERE name_hash=MD5('$filePath') FOR UPDATE", $fname, false );
01481             if ( mysql_num_rows( $res ) == 0 )
01482             {
01483                 $metaData = $generatingMetaData;
01484                 $metaData['name'] = $filePath;
01485                 $metaData['name_hash'] = md5( $filePath );
01486                 $metaData['name_trunk'] = $this->nameTrunk( $filePath, $metaData['scope'] );
01487                 $insertSQL = "INSERT INTO " . self::TABLE_METADATA . " ( " . implode( ', ', array_keys( $metaData ) ) . " ) " .
01488                              "VALUES( " . $this->_sqlList( $metaData ) . ")";
01489                 if ( !$this->_query( $insertSQL, $fname, true ) )
01490                 {
01491                     eZDebug::writeError("An error occured creating the metadata entry for $filePath", $fname );
01492                     $this->_rollback( $fname );
01493                     // @todo Throw an exception
01494                     return false;
01495                 }
01496                 // here we rename the actual FILE. The .generating file has been
01497                 // created on DFS, and should be renamed
01498                 if ( !$this->dfsbackend->renameOnDFS( $generatingFilePath, $filePath ) )
01499                 {
01500                     eZDebug::writeError("An error occured renaming DFS://$generatingFilePath to DFS://$filePath", $fname );
01501                     $this->_rollback( $fname );
01502                     // @todo Throw an exception
01503                     return false;
01504                 }
01505                 $this->_query( "DELETE FROM " . self::TABLE_METADATA . " WHERE name_hash=MD5('$generatingFilePath')", $fname, true );
01506             }
01507             // the original file exists: we move the generating data to this file
01508             // and update it
01509             else
01510             {
01511                 if ( !$this->dfsbackend->renameOnDFS( $generatingFilePath, $filePath ) )
01512                 {
01513                     eZDebug::writeError("An error occured renaming DFS://$generatingFilePath to DFS://$filePath", $fname );
01514                     $this->_rollback( $fname );
01515                     // @todo Throw an exception
01516                     return false;
01517                 }
01518 
01519                 $mtime = $generatingMetaData['mtime'];
01520                 $filesize = $generatingMetaData['size'];
01521                 if ( !$this->_query( "UPDATE " . self::TABLE_METADATA . " SET mtime = '{$mtime}', expired = 0, size = '{$filesize}' WHERE name_hash=MD5('$filePath')", $fname, true ) )
01522                 {
01523                     $this->_rollback( $fname );
01524                     // @todo Throw an exception
01525                     return false;
01526                 }
01527                 $this->_query( "DELETE FROM " . self::TABLE_METADATA . " WHERE name_hash=MD5('$generatingFilePath')", $fname, true );
01528             }
01529 
01530             $this->_commit( $fname );
01531         }
01532 
01533         return true;
01534     }
01535 
01536     /**
01537      * Checks if generation has timed out by looking for the .generating file
01538      * and comparing its timestamp to the one assigned when the file was created
01539      *
01540      * @param string $generatingFilePath
01541      * @param int    $generatingFileMtime
01542      *
01543      * @return bool true if the file didn't timeout, false otherwise
01544      */
01545     public function _checkCacheGenerationTimeout( $generatingFilePath, $generatingFileMtime )
01546     {
01547         $fname = "_checkCacheGenerationTimeout( $generatingFilePath, $generatingFileMtime )";
01548 
01549         // reporting
01550         eZDebug::accumulatorStart( 'mysql_cluster_query', 'MySQL Cluster', 'DB queries' );
01551         $time = microtime( true );
01552 
01553         $nameHash = $this->_md5( $generatingFilePath );
01554         $newMtime = time();
01555 
01556         // The update query will only succeed if the mtime wasn't changed in between
01557         $query = "UPDATE " . self::TABLE_METADATA . " SET mtime = $newMtime WHERE name_hash = {$nameHash} AND mtime = $generatingFileMtime";
01558         $res = mysql_query( $query, $this->db );
01559         if ( !$res )
01560         {
01561             // @todo Throw an exception
01562             $this->_error( $query, $fname );
01563             return false;
01564         }
01565         $numRows = mysql_affected_rows( $this->db );
01566 
01567         // reporting. Manual here since we don't use _query
01568         $time = microtime( true ) - $time;
01569         $this->_report( $query, $fname, $time, $numRows );
01570 
01571         // no rows affected or row updated with the same value
01572         // f.e a cache-block which takes less than 1 sec to get generated
01573         // if a line has been updated by the same  values, mysql_affected_rows
01574         // returns 0, and updates nothing, we need to extra check this,
01575         if( $numRows == 0 )
01576         {
01577             $query = "SELECT mtime FROM " . self::TABLE_METADATA . " WHERE name_hash = {$nameHash}";
01578             $res = mysql_query( $query, $this->db );
01579             if ( !$res )
01580                 return false;
01581             $row = mysql_fetch_row( $res );
01582             if ( isset( $row[0] ) and $row[0] == $generatingFileMtime );
01583             {
01584                 return true;
01585             }
01586 
01587             // @todo Check if an exception makes sense here
01588             return false;
01589         }
01590         // rows affected: mtime has changed, or row has been removed
01591         if ( $numRows == 1 )
01592         {
01593             return true;
01594         }
01595         else
01596         {
01597             eZDebugSetting::writeDebug( 'kernel-clustering', "No rows affected by query '$query', record has been modified", __METHOD__ );
01598             return false;
01599         }
01600     }
01601 
01602     /**
01603      * Aborts the cache generation process by removing the .generating file
01604      * @param string $filePath Real cache file path
01605      * @param string $generatingFilePath .generating cache file path
01606      * @return void
01607      */
01608     public function _abortCacheGeneration( $generatingFilePath )
01609     {
01610         $fname = "_abortCacheGeneration( $generatingFilePath )";
01611 
01612         $this->_begin( $fname );
01613 
01614         $sql = "DELETE FROM " . self::TABLE_METADATA . " WHERE name_hash = " . $this->_md5( $generatingFilePath );
01615         $this->_query( $sql, "_abortCacheGeneration( '$generatingFilePath' )" );
01616         $this->dfsbackend->delete( $generatingFilePath );
01617 
01618         $this->_commit( $fname );
01619     }
01620 
01621     /**
01622      * Returns the name_trunk for a file path
01623      * @param string $filePath
01624      * @param string $scope
01625      * @return string
01626      */
01627     static protected function nameTrunk( $filePath, $scope )
01628     {
01629         switch ( $scope )
01630         {
01631             case 'viewcache':
01632             {
01633                 $nameTrunk = substr( $filePath, 0, strrpos( $filePath, '-' ) + 1 );
01634             } break;
01635 
01636             case 'template-block':
01637             {
01638                 $templateBlockCacheDir = eZTemplateCacheBlock::templateBlockCacheDir();
01639                 $templateBlockPath = str_replace( $templateBlockCacheDir, '', $filePath );
01640                 if ( strstr( $templateBlockPath, 'subtree/' ) !== false )
01641                 {
01642                     // 6 = strlen( 'cache/' );
01643                     $len = strlen( $templateBlockCacheDir ) + strpos( $templateBlockPath, 'cache/' ) + 6;
01644                     $nameTrunk = substr( $filePath, 0, $len  );
01645                 }
01646                 else
01647                 {
01648                     $nameTrunk = $filePath;
01649                 }
01650             } break;
01651 
01652             default:
01653             {
01654                 $nameTrunk = $filePath;
01655             }
01656         }
01657         return $nameTrunk;
01658     }
01659 
01660     /**
01661      * Returns the remaining time, in seconds, before the generating file times
01662      * out
01663      *
01664      * @param resource $fileRow
01665      *
01666      * @return int Remaining generation seconds. A negative value indicates a timeout.
01667      */
01668     protected function remainingCacheGenerationTime( $row )
01669     {
01670         if( !isset( $row[0] ) )
01671             return -1;
01672 
01673         return ( $row[0] + self::$dbparams['cache_generation_timeout'] ) - time();
01674     }
01675 
01676     /**
01677      * Returns the list of expired files
01678      *
01679      * @param array $scopes Array of scopes to consider. At least one.
01680      * @param int $limit Max number of items. Set to false for unlimited.
01681      * @param int $expiry Number of seconds, only items older than this will be returned.
01682      *
01683      * @return array(filepath)
01684      *
01685      * @since 4.3
01686      */
01687     public function expiredFilesList( $scopes, $limit = array( 0, 100 ), $expiry = false )
01688     {
01689         if ( count( $scopes ) == 0 )
01690             throw new ezcBaseValueException( 'scopes', $scopes, "array of scopes", "parameter" );
01691 
01692         $scopeString = $this->_sqlList( $scopes );
01693         $query = "SELECT name FROM " . self::TABLE_METADATA . " WHERE expired = 1 AND scope IN( $scopeString )";
01694         if ( $expiry !== false )
01695         {
01696             $query .= ' AND mtime < ' . (time() - $expiry);
01697         }
01698         if ( $limit !== false )
01699         {
01700             $query .= " LIMIT {$limit[0]}, {$limit[1]}";
01701         }
01702         $res = $this->_query( $query, __METHOD__ );
01703         $filePathList = array();
01704         while ( $row = mysql_fetch_row( $res ) )
01705             $filePathList[] = $row[0];
01706 
01707         return $filePathList;
01708     }
01709 
01710     /**
01711      * DB connexion handle
01712      * @var handle
01713      */
01714     public $db = null;
01715 
01716     /**
01717      * DB connexion parameters
01718      * @var array
01719      */
01720     protected static $dbparams = null;
01721 
01722     /**
01723      * Amount of executed queries, for debugging purpose
01724      * @var int
01725      */
01726     protected $numQueries = 0;
01727 
01728     /**
01729      * Current transaction level.
01730      * Will be used to decide wether we can BEGIN (if it's the first BEGIN call)
01731      * or COMMIT (if we're commiting the last running transaction
01732      * @var int
01733      */
01734     protected $transactionCount = 0;
01735 
01736     /**
01737      * DB file table name
01738      * @var string
01739      */
01740     const TABLE_METADATA = 'ezdfsfile';
01741 
01742     /**
01743      * Distributed filesystem backend
01744      * @var eZDFSFileHandlerDFSBackend
01745      */
01746     protected $dfsbackend = null;
01747 }
01748 
01749 ?>