eZ Publish  [trunk]
mysql.php
Go to the documentation of this file.
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 ?>