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