|
eZ Publish
[trunk]
|
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 ?>