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