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