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