MediaWiki  REL1_20
FileBackendStore.php
Go to the documentation of this file.
00001 <?php
00038 abstract class FileBackendStore extends FileBackend {
00040         protected $memCache;
00042         protected $cheapCache; // Map of paths to small (RAM/disk) cache items
00044         protected $expensiveCache; // Map of paths to large (RAM/disk) cache items
00045 
00047         protected $shardViaHashLevels = array(); // (container name => config array)
00048 
00049         protected $maxFileSize = 4294967296; // integer bytes (4GiB)
00050 
00056         public function __construct( array $config ) {
00057                 parent::__construct( $config );
00058                 $this->memCache       = new EmptyBagOStuff(); // disabled by default
00059                 $this->cheapCache     = new ProcessCacheLRU( 300 );
00060                 $this->expensiveCache = new ProcessCacheLRU( 5 );
00061         }
00062 
00070         final public function maxFileSizeInternal() {
00071                 return $this->maxFileSize;
00072         }
00073 
00082         abstract public function isPathUsableInternal( $storagePath );
00083 
00100         final public function createInternal( array $params ) {
00101                 wfProfileIn( __METHOD__ );
00102                 wfProfileIn( __METHOD__ . '-' . $this->name );
00103                 if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
00104                         $status = Status::newFatal( 'backend-fail-maxsize',
00105                                 $params['dst'], $this->maxFileSizeInternal() );
00106                 } else {
00107                         $status = $this->doCreateInternal( $params );
00108                         $this->clearCache( array( $params['dst'] ) );
00109                         if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00110                                 $this->deleteFileCache( $params['dst'] ); // persistent cache
00111                         }
00112                 }
00113                 wfProfileOut( __METHOD__ . '-' . $this->name );
00114                 wfProfileOut( __METHOD__ );
00115                 return $status;
00116         }
00117 
00121         abstract protected function doCreateInternal( array $params );
00122 
00139         final public function storeInternal( array $params ) {
00140                 wfProfileIn( __METHOD__ );
00141                 wfProfileIn( __METHOD__ . '-' . $this->name );
00142                 if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
00143                         $status = Status::newFatal( 'backend-fail-maxsize',
00144                                 $params['dst'], $this->maxFileSizeInternal() );
00145                 } else {
00146                         $status = $this->doStoreInternal( $params );
00147                         $this->clearCache( array( $params['dst'] ) );
00148                         if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00149                                 $this->deleteFileCache( $params['dst'] ); // persistent cache
00150                         }
00151                 }
00152                 wfProfileOut( __METHOD__ . '-' . $this->name );
00153                 wfProfileOut( __METHOD__ );
00154                 return $status;
00155         }
00156 
00160         abstract protected function doStoreInternal( array $params );
00161 
00178         final public function copyInternal( array $params ) {
00179                 wfProfileIn( __METHOD__ );
00180                 wfProfileIn( __METHOD__ . '-' . $this->name );
00181                 $status = $this->doCopyInternal( $params );
00182                 $this->clearCache( array( $params['dst'] ) );
00183                 if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00184                         $this->deleteFileCache( $params['dst'] ); // persistent cache
00185                 }
00186                 wfProfileOut( __METHOD__ . '-' . $this->name );
00187                 wfProfileOut( __METHOD__ );
00188                 return $status;
00189         }
00190 
00194         abstract protected function doCopyInternal( array $params );
00195 
00210         final public function deleteInternal( array $params ) {
00211                 wfProfileIn( __METHOD__ );
00212                 wfProfileIn( __METHOD__ . '-' . $this->name );
00213                 $status = $this->doDeleteInternal( $params );
00214                 $this->clearCache( array( $params['src'] ) );
00215                 $this->deleteFileCache( $params['src'] ); // persistent cache
00216                 wfProfileOut( __METHOD__ . '-' . $this->name );
00217                 wfProfileOut( __METHOD__ );
00218                 return $status;
00219         }
00220 
00224         abstract protected function doDeleteInternal( array $params );
00225 
00242         final public function moveInternal( array $params ) {
00243                 wfProfileIn( __METHOD__ );
00244                 wfProfileIn( __METHOD__ . '-' . $this->name );
00245                 $status = $this->doMoveInternal( $params );
00246                 $this->clearCache( array( $params['src'], $params['dst'] ) );
00247                 $this->deleteFileCache( $params['src'] ); // persistent cache
00248                 if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00249                         $this->deleteFileCache( $params['dst'] ); // persistent cache
00250                 }
00251                 wfProfileOut( __METHOD__ . '-' . $this->name );
00252                 wfProfileOut( __METHOD__ );
00253                 return $status;
00254         }
00255 
00260         protected function doMoveInternal( array $params ) {
00261                 unset( $params['async'] ); // two steps, won't work here :)
00262                 // Copy source to dest
00263                 $status = $this->copyInternal( $params );
00264                 if ( $status->isOK() ) {
00265                         // Delete source (only fails due to races or medium going down)
00266                         $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) );
00267                         $status->setResult( true, $status->value ); // ignore delete() errors
00268                 }
00269                 return $status;
00270         }
00271 
00279         final public function nullInternal( array $params ) {
00280                 return Status::newGood();
00281         }
00282 
00287         final public function concatenate( array $params ) {
00288                 wfProfileIn( __METHOD__ );
00289                 wfProfileIn( __METHOD__ . '-' . $this->name );
00290                 $status = Status::newGood();
00291 
00292                 // Try to lock the source files for the scope of this function
00293                 $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
00294                 if ( $status->isOK() ) {
00295                         // Actually do the file concatenation...
00296                         $start_time = microtime( true );
00297                         $status->merge( $this->doConcatenate( $params ) );
00298                         $sec = microtime( true ) - $start_time;
00299                         if ( !$status->isOK() ) {
00300                                 wfDebugLog( 'FileOperation', get_class( $this ) . " failed to concatenate " .
00301                                         count( $params['srcs'] ) . " file(s) [$sec sec]" );
00302                         }
00303                 }
00304 
00305                 wfProfileOut( __METHOD__ . '-' . $this->name );
00306                 wfProfileOut( __METHOD__ );
00307                 return $status;
00308         }
00309 
00314         protected function doConcatenate( array $params ) {
00315                 $status = Status::newGood();
00316                 $tmpPath = $params['dst']; // convenience
00317 
00318                 // Check that the specified temp file is valid...
00319                 wfSuppressWarnings();
00320                 $ok = ( is_file( $tmpPath ) && !filesize( $tmpPath ) );
00321                 wfRestoreWarnings();
00322                 if ( !$ok ) { // not present or not empty
00323                         $status->fatal( 'backend-fail-opentemp', $tmpPath );
00324                         return $status;
00325                 }
00326 
00327                 // Build up the temp file using the source chunks (in order)...
00328                 $tmpHandle = fopen( $tmpPath, 'ab' );
00329                 if ( $tmpHandle === false ) {
00330                         $status->fatal( 'backend-fail-opentemp', $tmpPath );
00331                         return $status;
00332                 }
00333                 foreach ( $params['srcs'] as $virtualSource ) {
00334                         // Get a local FS version of the chunk
00335                         $tmpFile = $this->getLocalReference( array( 'src' => $virtualSource ) );
00336                         if ( !$tmpFile ) {
00337                                 $status->fatal( 'backend-fail-read', $virtualSource );
00338                                 return $status;
00339                         }
00340                         // Get a handle to the local FS version
00341                         $sourceHandle = fopen( $tmpFile->getPath(), 'r' );
00342                         if ( $sourceHandle === false ) {
00343                                 fclose( $tmpHandle );
00344                                 $status->fatal( 'backend-fail-read', $virtualSource );
00345                                 return $status;
00346                         }
00347                         // Append chunk to file (pass chunk size to avoid magic quotes)
00348                         if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
00349                                 fclose( $sourceHandle );
00350                                 fclose( $tmpHandle );
00351                                 $status->fatal( 'backend-fail-writetemp', $tmpPath );
00352                                 return $status;
00353                         }
00354                         fclose( $sourceHandle );
00355                 }
00356                 if ( !fclose( $tmpHandle ) ) {
00357                         $status->fatal( 'backend-fail-closetemp', $tmpPath );
00358                         return $status;
00359                 }
00360 
00361                 clearstatcache(); // temp file changed
00362 
00363                 return $status;
00364         }
00365 
00370         final protected function doPrepare( array $params ) {
00371                 wfProfileIn( __METHOD__ );
00372                 wfProfileIn( __METHOD__ . '-' . $this->name );
00373 
00374                 $status = Status::newGood();
00375                 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00376                 if ( $dir === null ) {
00377                         $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
00378                         wfProfileOut( __METHOD__ . '-' . $this->name );
00379                         wfProfileOut( __METHOD__ );
00380                         return $status; // invalid storage path
00381                 }
00382 
00383                 if ( $shard !== null ) { // confined to a single container/shard
00384                         $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
00385                 } else { // directory is on several shards
00386                         wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00387                         list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
00388                         foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
00389                                 $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
00390                         }
00391                 }
00392 
00393                 wfProfileOut( __METHOD__ . '-' . $this->name );
00394                 wfProfileOut( __METHOD__ );
00395                 return $status;
00396         }
00397 
00402         protected function doPrepareInternal( $container, $dir, array $params ) {
00403                 return Status::newGood();
00404         }
00405 
00410         final protected function doSecure( array $params ) {
00411                 wfProfileIn( __METHOD__ );
00412                 wfProfileIn( __METHOD__ . '-' . $this->name );
00413                 $status = Status::newGood();
00414 
00415                 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00416                 if ( $dir === null ) {
00417                         $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
00418                         wfProfileOut( __METHOD__ . '-' . $this->name );
00419                         wfProfileOut( __METHOD__ );
00420                         return $status; // invalid storage path
00421                 }
00422 
00423                 if ( $shard !== null ) { // confined to a single container/shard
00424                         $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
00425                 } else { // directory is on several shards
00426                         wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00427                         list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
00428                         foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
00429                                 $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
00430                         }
00431                 }
00432 
00433                 wfProfileOut( __METHOD__ . '-' . $this->name );
00434                 wfProfileOut( __METHOD__ );
00435                 return $status;
00436         }
00437 
00442         protected function doSecureInternal( $container, $dir, array $params ) {
00443                 return Status::newGood();
00444         }
00445 
00450         final protected function doPublish( array $params ) {
00451                 wfProfileIn( __METHOD__ );
00452                 wfProfileIn( __METHOD__ . '-' . $this->name );
00453                 $status = Status::newGood();
00454 
00455                 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00456                 if ( $dir === null ) {
00457                         $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
00458                         wfProfileOut( __METHOD__ . '-' . $this->name );
00459                         wfProfileOut( __METHOD__ );
00460                         return $status; // invalid storage path
00461                 }
00462 
00463                 if ( $shard !== null ) { // confined to a single container/shard
00464                         $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
00465                 } else { // directory is on several shards
00466                         wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00467                         list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
00468                         foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
00469                                 $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
00470                         }
00471                 }
00472 
00473                 wfProfileOut( __METHOD__ . '-' . $this->name );
00474                 wfProfileOut( __METHOD__ );
00475                 return $status;
00476         }
00477 
00482         protected function doPublishInternal( $container, $dir, array $params ) {
00483                 return Status::newGood();
00484         }
00485 
00490         final protected function doClean( array $params ) {
00491                 wfProfileIn( __METHOD__ );
00492                 wfProfileIn( __METHOD__ . '-' . $this->name );
00493                 $status = Status::newGood();
00494 
00495                 // Recursive: first delete all empty subdirs recursively
00496                 if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
00497                         $subDirsRel = $this->getTopDirectoryList( array( 'dir' => $params['dir'] ) );
00498                         if ( $subDirsRel !== null ) { // no errors
00499                                 foreach ( $subDirsRel as $subDirRel ) {
00500                                         $subDir = $params['dir'] . "/{$subDirRel}"; // full path
00501                                         $status->merge( $this->doClean( array( 'dir' => $subDir ) + $params ) );
00502                                 }
00503                         }
00504                 }
00505 
00506                 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00507                 if ( $dir === null ) {
00508                         $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
00509                         wfProfileOut( __METHOD__ . '-' . $this->name );
00510                         wfProfileOut( __METHOD__ );
00511                         return $status; // invalid storage path
00512                 }
00513 
00514                 // Attempt to lock this directory...
00515                 $filesLockEx = array( $params['dir'] );
00516                 $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
00517                 if ( !$status->isOK() ) {
00518                         wfProfileOut( __METHOD__ . '-' . $this->name );
00519                         wfProfileOut( __METHOD__ );
00520                         return $status; // abort
00521                 }
00522 
00523                 if ( $shard !== null ) { // confined to a single container/shard
00524                         $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
00525                         $this->deleteContainerCache( $fullCont ); // purge cache
00526                 } else { // directory is on several shards
00527                         wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00528                         list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
00529                         foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
00530                                 $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
00531                                 $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
00532                         }
00533                 }
00534 
00535                 wfProfileOut( __METHOD__ . '-' . $this->name );
00536                 wfProfileOut( __METHOD__ );
00537                 return $status;
00538         }
00539 
00544         protected function doCleanInternal( $container, $dir, array $params ) {
00545                 return Status::newGood();
00546         }
00547 
00552         final public function fileExists( array $params ) {
00553                 wfProfileIn( __METHOD__ );
00554                 wfProfileIn( __METHOD__ . '-' . $this->name );
00555                 $stat = $this->getFileStat( $params );
00556                 wfProfileOut( __METHOD__ . '-' . $this->name );
00557                 wfProfileOut( __METHOD__ );
00558                 return ( $stat === null ) ? null : (bool)$stat; // null => failure
00559         }
00560 
00565         final public function getFileTimestamp( array $params ) {
00566                 wfProfileIn( __METHOD__ );
00567                 wfProfileIn( __METHOD__ . '-' . $this->name );
00568                 $stat = $this->getFileStat( $params );
00569                 wfProfileOut( __METHOD__ . '-' . $this->name );
00570                 wfProfileOut( __METHOD__ );
00571                 return $stat ? $stat['mtime'] : false;
00572         }
00573 
00578         final public function getFileSize( array $params ) {
00579                 wfProfileIn( __METHOD__ );
00580                 wfProfileIn( __METHOD__ . '-' . $this->name );
00581                 $stat = $this->getFileStat( $params );
00582                 wfProfileOut( __METHOD__ . '-' . $this->name );
00583                 wfProfileOut( __METHOD__ );
00584                 return $stat ? $stat['size'] : false;
00585         }
00586 
00591         final public function getFileStat( array $params ) {
00592                 $path = self::normalizeStoragePath( $params['src'] );
00593                 if ( $path === null ) {
00594                         return false; // invalid storage path
00595                 }
00596                 wfProfileIn( __METHOD__ );
00597                 wfProfileIn( __METHOD__ . '-' . $this->name );
00598                 $latest = !empty( $params['latest'] ); // use latest data?
00599                 if ( !$this->cheapCache->has( $path, 'stat' ) ) {
00600                         $this->primeFileCache( array( $path ) ); // check persistent cache
00601                 }
00602                 if ( $this->cheapCache->has( $path, 'stat' ) ) {
00603                         $stat = $this->cheapCache->get( $path, 'stat' );
00604                         // If we want the latest data, check that this cached
00605                         // value was in fact fetched with the latest available data.
00606                         if ( !$latest || $stat['latest'] ) {
00607                                 wfProfileOut( __METHOD__ . '-' . $this->name );
00608                                 wfProfileOut( __METHOD__ );
00609                                 return $stat;
00610                         }
00611                 }
00612                 wfProfileIn( __METHOD__ . '-miss' );
00613                 wfProfileIn( __METHOD__ . '-miss-' . $this->name );
00614                 $stat = $this->doGetFileStat( $params );
00615                 wfProfileOut( __METHOD__ . '-miss-' . $this->name );
00616                 wfProfileOut( __METHOD__ . '-miss' );
00617                 if ( is_array( $stat ) ) { // don't cache negatives
00618                         $stat['latest'] = $latest;
00619                         $this->cheapCache->set( $path, 'stat', $stat );
00620                         $this->setFileCache( $path, $stat ); // update persistent cache
00621                         if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
00622                                 $this->cheapCache->set( $path, 'sha1',
00623                                         array( 'hash' => $stat['sha1'], 'latest' => $latest ) );
00624                         }
00625                 } else {
00626                         wfDebug( __METHOD__ . ": File $path does not exist.\n" );
00627                 }
00628                 wfProfileOut( __METHOD__ . '-' . $this->name );
00629                 wfProfileOut( __METHOD__ );
00630                 return $stat;
00631         }
00632 
00636         abstract protected function doGetFileStat( array $params );
00637 
00642         public function getFileContents( array $params ) {
00643                 wfProfileIn( __METHOD__ );
00644                 wfProfileIn( __METHOD__ . '-' . $this->name );
00645                 $tmpFile = $this->getLocalReference( $params );
00646                 if ( !$tmpFile ) {
00647                         wfProfileOut( __METHOD__ . '-' . $this->name );
00648                         wfProfileOut( __METHOD__ );
00649                         return false;
00650                 }
00651                 wfSuppressWarnings();
00652                 $data = file_get_contents( $tmpFile->getPath() );
00653                 wfRestoreWarnings();
00654                 wfProfileOut( __METHOD__ . '-' . $this->name );
00655                 wfProfileOut( __METHOD__ );
00656                 return $data;
00657         }
00658 
00663         final public function getFileSha1Base36( array $params ) {
00664                 $path = self::normalizeStoragePath( $params['src'] );
00665                 if ( $path === null ) {
00666                         return false; // invalid storage path
00667                 }
00668                 wfProfileIn( __METHOD__ );
00669                 wfProfileIn( __METHOD__ . '-' . $this->name );
00670                 $latest = !empty( $params['latest'] ); // use latest data?
00671                 if ( $this->cheapCache->has( $path, 'sha1' ) ) {
00672                         $stat = $this->cheapCache->get( $path, 'sha1' );
00673                         // If we want the latest data, check that this cached
00674                         // value was in fact fetched with the latest available data.
00675                         if ( !$latest || $stat['latest'] ) {
00676                                 wfProfileOut( __METHOD__ . '-' . $this->name );
00677                                 wfProfileOut( __METHOD__ );
00678                                 return $stat['hash'];
00679                         }
00680                 }
00681                 wfProfileIn( __METHOD__ . '-miss' );
00682                 wfProfileIn( __METHOD__ . '-miss-' . $this->name );
00683                 $hash = $this->doGetFileSha1Base36( $params );
00684                 wfProfileOut( __METHOD__ . '-miss-' . $this->name );
00685                 wfProfileOut( __METHOD__ . '-miss' );
00686                 if ( $hash ) { // don't cache negatives
00687                         $this->cheapCache->set( $path, 'sha1',
00688                                 array( 'hash' => $hash, 'latest' => $latest ) );
00689                 }
00690                 wfProfileOut( __METHOD__ . '-' . $this->name );
00691                 wfProfileOut( __METHOD__ );
00692                 return $hash;
00693         }
00694 
00699         protected function doGetFileSha1Base36( array $params ) {
00700                 $fsFile = $this->getLocalReference( $params );
00701                 if ( !$fsFile ) {
00702                         return false;
00703                 } else {
00704                         return $fsFile->getSha1Base36();
00705                 }
00706         }
00707 
00712         final public function getFileProps( array $params ) {
00713                 wfProfileIn( __METHOD__ );
00714                 wfProfileIn( __METHOD__ . '-' . $this->name );
00715                 $fsFile = $this->getLocalReference( $params );
00716                 $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
00717                 wfProfileOut( __METHOD__ . '-' . $this->name );
00718                 wfProfileOut( __METHOD__ );
00719                 return $props;
00720         }
00721 
00726         public function getLocalReference( array $params ) {
00727                 $path = self::normalizeStoragePath( $params['src'] );
00728                 if ( $path === null ) {
00729                         return null; // invalid storage path
00730                 }
00731                 wfProfileIn( __METHOD__ );
00732                 wfProfileIn( __METHOD__ . '-' . $this->name );
00733                 $latest = !empty( $params['latest'] ); // use latest data?
00734                 if ( $this->expensiveCache->has( $path, 'localRef' ) ) {
00735                         $val = $this->expensiveCache->get( $path, 'localRef' );
00736                         // If we want the latest data, check that this cached
00737                         // value was in fact fetched with the latest available data.
00738                         if ( !$latest || $val['latest'] ) {
00739                                 wfProfileOut( __METHOD__ . '-' . $this->name );
00740                                 wfProfileOut( __METHOD__ );
00741                                 return $val['object'];
00742                         }
00743                 }
00744                 $tmpFile = $this->getLocalCopy( $params );
00745                 if ( $tmpFile ) { // don't cache negatives
00746                         $this->expensiveCache->set( $path, 'localRef',
00747                                 array( 'object' => $tmpFile, 'latest' => $latest ) );
00748                 }
00749                 wfProfileOut( __METHOD__ . '-' . $this->name );
00750                 wfProfileOut( __METHOD__ );
00751                 return $tmpFile;
00752         }
00753 
00758         final public function streamFile( array $params ) {
00759                 wfProfileIn( __METHOD__ );
00760                 wfProfileIn( __METHOD__ . '-' . $this->name );
00761                 $status = Status::newGood();
00762 
00763                 $info = $this->getFileStat( $params );
00764                 if ( !$info ) { // let StreamFile handle the 404
00765                         $status->fatal( 'backend-fail-notexists', $params['src'] );
00766                 }
00767 
00768                 // Set output buffer and HTTP headers for stream
00769                 $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : array();
00770                 $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
00771                 if ( $res == StreamFile::NOT_MODIFIED ) {
00772                         // do nothing; client cache is up to date
00773                 } elseif ( $res == StreamFile::READY_STREAM ) {
00774                         wfProfileIn( __METHOD__ . '-send' );
00775                         wfProfileIn( __METHOD__ . '-send-' . $this->name );
00776                         $status = $this->doStreamFile( $params );
00777                         wfProfileOut( __METHOD__ . '-send-' . $this->name );
00778                         wfProfileOut( __METHOD__ . '-send' );
00779                 } else {
00780                         $status->fatal( 'backend-fail-stream', $params['src'] );
00781                 }
00782 
00783                 wfProfileOut( __METHOD__ . '-' . $this->name );
00784                 wfProfileOut( __METHOD__ );
00785                 return $status;
00786         }
00787 
00792         protected function doStreamFile( array $params ) {
00793                 $status = Status::newGood();
00794 
00795                 $fsFile = $this->getLocalReference( $params );
00796                 if ( !$fsFile ) {
00797                         $status->fatal( 'backend-fail-stream', $params['src'] );
00798                 } elseif ( !readfile( $fsFile->getPath() ) ) {
00799                         $status->fatal( 'backend-fail-stream', $params['src'] );
00800                 }
00801 
00802                 return $status;
00803         }
00804 
00809         final public function directoryExists( array $params ) {
00810                 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00811                 if ( $dir === null ) {
00812                         return false; // invalid storage path
00813                 }
00814                 if ( $shard !== null ) { // confined to a single container/shard
00815                         return $this->doDirectoryExists( $fullCont, $dir, $params );
00816                 } else { // directory is on several shards
00817                         wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00818                         list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
00819                         $res = false; // response
00820                         foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
00821                                 $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
00822                                 if ( $exists ) {
00823                                         $res = true;
00824                                         break; // found one!
00825                                 } elseif ( $exists === null ) { // error?
00826                                         $res = null; // if we don't find anything, it is indeterminate
00827                                 }
00828                         }
00829                         return $res;
00830                 }
00831         }
00832 
00841         abstract protected function doDirectoryExists( $container, $dir, array $params );
00842 
00847         final public function getDirectoryList( array $params ) {
00848                 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00849                 if ( $dir === null ) { // invalid storage path
00850                         return null;
00851                 }
00852                 if ( $shard !== null ) {
00853                         // File listing is confined to a single container/shard
00854                         return $this->getDirectoryListInternal( $fullCont, $dir, $params );
00855                 } else {
00856                         wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00857                         // File listing spans multiple containers/shards
00858                         list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
00859                         return new FileBackendStoreShardDirIterator( $this,
00860                                 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
00861                 }
00862         }
00863 
00874         abstract public function getDirectoryListInternal( $container, $dir, array $params );
00875 
00880         final public function getFileList( array $params ) {
00881                 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00882                 if ( $dir === null ) { // invalid storage path
00883                         return null;
00884                 }
00885                 if ( $shard !== null ) {
00886                         // File listing is confined to a single container/shard
00887                         return $this->getFileListInternal( $fullCont, $dir, $params );
00888                 } else {
00889                         wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00890                         // File listing spans multiple containers/shards
00891                         list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
00892                         return new FileBackendStoreShardFileIterator( $this,
00893                                 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
00894                 }
00895         }
00896 
00907         abstract public function getFileListInternal( $container, $dir, array $params );
00908 
00920         final public function getOperationsInternal( array $ops ) {
00921                 $supportedOps = array(
00922                         'store'       => 'StoreFileOp',
00923                         'copy'        => 'CopyFileOp',
00924                         'move'        => 'MoveFileOp',
00925                         'delete'      => 'DeleteFileOp',
00926                         'create'      => 'CreateFileOp',
00927                         'null'        => 'NullFileOp'
00928                 );
00929 
00930                 $performOps = array(); // array of FileOp objects
00931                 // Build up ordered array of FileOps...
00932                 foreach ( $ops as $operation ) {
00933                         $opName = $operation['op'];
00934                         if ( isset( $supportedOps[$opName] ) ) {
00935                                 $class = $supportedOps[$opName];
00936                                 // Get params for this operation
00937                                 $params = $operation;
00938                                 // Append the FileOp class
00939                                 $performOps[] = new $class( $this, $params );
00940                         } else {
00941                                 throw new MWException( "Operation '$opName' is not supported." );
00942                         }
00943                 }
00944 
00945                 return $performOps;
00946         }
00947 
00956         final public function getPathsToLockForOpsInternal( array $performOps ) {
00957                 // Build up a list of files to lock...
00958                 $paths = array( 'sh' => array(), 'ex' => array() );
00959                 foreach ( $performOps as $fileOp ) {
00960                         $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
00961                         $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
00962                 }
00963                 // Optimization: if doing an EX lock anyway, don't also set an SH one
00964                 $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
00965                 // Get a shared lock on the parent directory of each path changed
00966                 $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
00967 
00968                 return $paths;
00969         }
00970 
00975         public function getScopedLocksForOps( array $ops, Status $status ) {
00976                 $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
00977                 return array(
00978                         $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status ),
00979                         $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status )
00980                 );
00981         }
00982 
00987         final protected function doOperationsInternal( array $ops, array $opts ) {
00988                 wfProfileIn( __METHOD__ );
00989                 wfProfileIn( __METHOD__ . '-' . $this->name );
00990                 $status = Status::newGood();
00991 
00992                 // Build up a list of FileOps...
00993                 $performOps = $this->getOperationsInternal( $ops );
00994 
00995                 // Acquire any locks as needed...
00996                 if ( empty( $opts['nonLocking'] ) ) {
00997                         // Build up a list of files to lock...
00998                         $paths = $this->getPathsToLockForOpsInternal( $performOps );
00999                         // Try to lock those files for the scope of this function...
01000                         $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
01001                         $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
01002                         if ( !$status->isOK() ) {
01003                                 wfProfileOut( __METHOD__ . '-' . $this->name );
01004                                 wfProfileOut( __METHOD__ );
01005                                 return $status; // abort
01006                         }
01007                 }
01008 
01009                 // Clear any file cache entries (after locks acquired)
01010                 if ( empty( $opts['preserveCache'] ) ) {
01011                         $this->clearCache();
01012                 }
01013 
01014                 // Load from the persistent file and container caches
01015                 $this->primeFileCache( $performOps );
01016                 $this->primeContainerCache( $performOps );
01017 
01018                 // Actually attempt the operation batch...
01019                 $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
01020 
01021                 // Merge errors into status fields
01022                 $status->merge( $subStatus );
01023                 $status->success = $subStatus->success; // not done in merge()
01024 
01025                 wfProfileOut( __METHOD__ . '-' . $this->name );
01026                 wfProfileOut( __METHOD__ );
01027                 return $status;
01028         }
01029 
01035         final protected function doQuickOperationsInternal( array $ops ) {
01036                 wfProfileIn( __METHOD__ );
01037                 wfProfileIn( __METHOD__ . '-' . $this->name );
01038                 $status = Status::newGood();
01039 
01040                 $supportedOps = array( 'create', 'store', 'copy', 'move', 'delete', 'null' );
01041                 $async = ( $this->parallelize === 'implicit' );
01042                 $maxConcurrency = $this->concurrency; // throttle
01043 
01044                 $statuses = array(); // array of (index => Status)
01045                 $fileOpHandles = array(); // list of (index => handle) arrays
01046                 $curFileOpHandles = array(); // current handle batch
01047                 // Perform the sync-only ops and build up op handles for the async ops...
01048                 foreach ( $ops as $index => $params ) {
01049                         if ( !in_array( $params['op'], $supportedOps ) ) {
01050                                 wfProfileOut( __METHOD__ . '-' . $this->name );
01051                                 wfProfileOut( __METHOD__ );
01052                                 throw new MWException( "Operation '{$params['op']}' is not supported." );
01053                         }
01054                         $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
01055                         $subStatus = $this->$method( array( 'async' => $async ) + $params );
01056                         if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
01057                                 if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
01058                                         $fileOpHandles[] = $curFileOpHandles; // push this batch
01059                                         $curFileOpHandles = array();
01060                                 }
01061                                 $curFileOpHandles[$index] = $subStatus->value; // keep index
01062                         } else { // error or completed
01063                                 $statuses[$index] = $subStatus; // keep index
01064                         }
01065                 }
01066                 if ( count( $curFileOpHandles ) ) {
01067                         $fileOpHandles[] = $curFileOpHandles; // last batch
01068                 }
01069                 // Do all the async ops that can be done concurrently...
01070                 foreach ( $fileOpHandles as $fileHandleBatch ) {
01071                         $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
01072                 }
01073                 // Marshall and merge all the responses...
01074                 foreach ( $statuses as $index => $subStatus ) {
01075                         $status->merge( $subStatus );
01076                         if ( $subStatus->isOK() ) {
01077                                 $status->success[$index] = true;
01078                                 ++$status->successCount;
01079                         } else {
01080                                 $status->success[$index] = false;
01081                                 ++$status->failCount;
01082                         }
01083                 }
01084 
01085                 wfProfileOut( __METHOD__ . '-' . $this->name );
01086                 wfProfileOut( __METHOD__ );
01087                 return $status;
01088         }
01089 
01099         final public function executeOpHandlesInternal( array $fileOpHandles ) {
01100                 wfProfileIn( __METHOD__ );
01101                 wfProfileIn( __METHOD__ . '-' . $this->name );
01102                 foreach ( $fileOpHandles as $fileOpHandle ) {
01103                         if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
01104                                 throw new MWException( "Given a non-FileBackendStoreOpHandle object." );
01105                         } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
01106                                 throw new MWException( "Given a FileBackendStoreOpHandle for the wrong backend." );
01107                         }
01108                 }
01109                 $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
01110                 foreach ( $fileOpHandles as $fileOpHandle ) {
01111                         $fileOpHandle->closeResources();
01112                 }
01113                 wfProfileOut( __METHOD__ . '-' . $this->name );
01114                 wfProfileOut( __METHOD__ );
01115                 return $res;
01116         }
01117 
01122         protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
01123                 foreach ( $fileOpHandles as $fileOpHandle ) { // OK if empty
01124                         throw new MWException( "This backend supports no asynchronous operations." );
01125                 }
01126                 return array();
01127         }
01128 
01132         final public function preloadCache( array $paths ) {
01133                 $fullConts = array(); // full container names
01134                 foreach ( $paths as $path ) {
01135                         list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
01136                         $fullConts[] = $fullCont;
01137                 }
01138                 // Load from the persistent file and container caches
01139                 $this->primeContainerCache( $fullConts );
01140                 $this->primeFileCache( $paths );
01141         }
01142 
01146         final public function clearCache( array $paths = null ) {
01147                 if ( is_array( $paths ) ) {
01148                         $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
01149                         $paths = array_filter( $paths, 'strlen' ); // remove nulls
01150                 }
01151                 if ( $paths === null ) {
01152                         $this->cheapCache->clear();
01153                         $this->expensiveCache->clear();
01154                 } else {
01155                         foreach ( $paths as $path ) {
01156                                 $this->cheapCache->clear( $path );
01157                                 $this->expensiveCache->clear( $path );
01158                         }
01159                 }
01160                 $this->doClearCache( $paths );
01161         }
01162 
01171         protected function doClearCache( array $paths = null ) {}
01172 
01180         abstract protected function directoriesAreVirtual();
01181 
01189         final protected static function isValidContainerName( $container ) {
01190                 // This accounts for Swift and S3 restrictions while leaving room
01191                 // for things like '.xxx' (hex shard chars) or '.seg' (segments).
01192                 // This disallows directory separators or traversal characters.
01193                 // Note that matching strings URL encode to the same string;
01194                 // in Swift, the length restriction is *after* URL encoding.
01195                 return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container );
01196         }
01197 
01211         final protected function resolveStoragePath( $storagePath ) {
01212                 list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
01213                 if ( $backend === $this->name ) { // must be for this backend
01214                         $relPath = self::normalizeContainerPath( $relPath );
01215                         if ( $relPath !== null ) {
01216                                 // Get shard for the normalized path if this container is sharded
01217                                 $cShard = $this->getContainerShard( $container, $relPath );
01218                                 // Validate and sanitize the relative path (backend-specific)
01219                                 $relPath = $this->resolveContainerPath( $container, $relPath );
01220                                 if ( $relPath !== null ) {
01221                                         // Prepend any wiki ID prefix to the container name
01222                                         $container = $this->fullContainerName( $container );
01223                                         if ( self::isValidContainerName( $container ) ) {
01224                                                 // Validate and sanitize the container name (backend-specific)
01225                                                 $container = $this->resolveContainerName( "{$container}{$cShard}" );
01226                                                 if ( $container !== null ) {
01227                                                         return array( $container, $relPath, $cShard );
01228                                                 }
01229                                         }
01230                                 }
01231                         }
01232                 }
01233                 return array( null, null, null );
01234         }
01235 
01245         final protected function resolveStoragePathReal( $storagePath ) {
01246                 list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
01247                 if ( $cShard !== null ) {
01248                         return array( $container, $relPath );
01249                 }
01250                 return array( null, null );
01251         }
01252 
01261         final protected function getContainerShard( $container, $relPath ) {
01262                 list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
01263                 if ( $levels == 1 || $levels == 2 ) {
01264                         // Hash characters are either base 16 or 36
01265                         $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
01266                         // Get a regex that represents the shard portion of paths.
01267                         // The concatenation of the captures gives us the shard.
01268                         if ( $levels === 1 ) { // 16 or 36 shards per container
01269                                 $hashDirRegex = '(' . $char . ')';
01270                         } else { // 256 or 1296 shards per container
01271                                 if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
01272                                         $hashDirRegex = $char . '/(' . $char . '{2})';
01273                                 } else { // short hash dir format (e.g. "a/b/c")
01274                                         $hashDirRegex = '(' . $char . ')/(' . $char . ')';
01275                                 }
01276                         }
01277                         // Allow certain directories to be above the hash dirs so as
01278                         // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
01279                         // They must be 2+ chars to avoid any hash directory ambiguity.
01280                         $m = array();
01281                         if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
01282                                 return '.' . implode( '', array_slice( $m, 1 ) );
01283                         }
01284                         return null; // failed to match
01285                 }
01286                 return ''; // no sharding
01287         }
01288 
01297         final public function isSingleShardPathInternal( $storagePath ) {
01298                 list( $c, $r, $shard ) = $this->resolveStoragePath( $storagePath );
01299                 return ( $shard !== null );
01300         }
01301 
01310         final protected function getContainerHashLevels( $container ) {
01311                 if ( isset( $this->shardViaHashLevels[$container] ) ) {
01312                         $config = $this->shardViaHashLevels[$container];
01313                         $hashLevels = (int)$config['levels'];
01314                         if ( $hashLevels == 1 || $hashLevels == 2 ) {
01315                                 $hashBase = (int)$config['base'];
01316                                 if ( $hashBase == 16 || $hashBase == 36 ) {
01317                                         return array( $hashLevels, $hashBase, $config['repeat'] );
01318                                 }
01319                         }
01320                 }
01321                 return array( 0, 0, false ); // no sharding
01322         }
01323 
01330         final protected function getContainerSuffixes( $container ) {
01331                 $shards = array();
01332                 list( $digits, $base ) = $this->getContainerHashLevels( $container );
01333                 if ( $digits > 0 ) {
01334                         $numShards = pow( $base, $digits );
01335                         for ( $index = 0; $index < $numShards; $index++ ) {
01336                                 $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits );
01337                         }
01338                 }
01339                 return $shards;
01340         }
01341 
01348         final protected function fullContainerName( $container ) {
01349                 if ( $this->wikiId != '' ) {
01350                         return "{$this->wikiId}-$container";
01351                 } else {
01352                         return $container;
01353                 }
01354         }
01355 
01364         protected function resolveContainerName( $container ) {
01365                 return $container;
01366         }
01367 
01378         protected function resolveContainerPath( $container, $relStoragePath ) {
01379                 return $relStoragePath;
01380         }
01381 
01388         private function containerCacheKey( $container ) {
01389                 return wfMemcKey( 'backend', $this->getName(), 'container', $container );
01390         }
01391 
01398         final protected function setContainerCache( $container, $val ) {
01399                 $this->memCache->add( $this->containerCacheKey( $container ), $val, 14*86400 );
01400         }
01401 
01408         final protected function deleteContainerCache( $container ) {
01409                 if ( !$this->memCache->set( $this->containerCacheKey( $container ), 'PURGED', 300 ) ) {
01410                         trigger_error( "Unable to delete stat cache for container $container." );
01411                 }
01412         }
01413 
01421         final protected function primeContainerCache( array $items ) {
01422                 wfProfileIn( __METHOD__ );
01423                 wfProfileIn( __METHOD__ . '-' . $this->name );
01424 
01425                 $paths = array(); // list of storage paths
01426                 $contNames = array(); // (cache key => resolved container name)
01427                 // Get all the paths/containers from the items...
01428                 foreach ( $items as $item ) {
01429                         if ( $item instanceof FileOp ) {
01430                                 $paths = array_merge( $paths, $item->storagePathsRead() );
01431                                 $paths = array_merge( $paths, $item->storagePathsChanged() );
01432                         } elseif ( self::isStoragePath( $item ) ) {
01433                                 $paths[] = $item;
01434                         } elseif ( is_string( $item ) ) { // full container name
01435                                 $contNames[$this->containerCacheKey( $item )] = $item;
01436                         }
01437                 }
01438                 // Get all the corresponding cache keys for paths...
01439                 foreach ( $paths as $path ) {
01440                         list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
01441                         if ( $fullCont !== null ) { // valid path for this backend
01442                                 $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
01443                         }
01444                 }
01445 
01446                 $contInfo = array(); // (resolved container name => cache value)
01447                 // Get all cache entries for these container cache keys...
01448                 $values = $this->memCache->getMulti( array_keys( $contNames ) );
01449                 foreach ( $values as $cacheKey => $val ) {
01450                         $contInfo[$contNames[$cacheKey]] = $val;
01451                 }
01452 
01453                 // Populate the container process cache for the backend...
01454                 $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
01455 
01456                 wfProfileOut( __METHOD__ . '-' . $this->name );
01457                 wfProfileOut( __METHOD__ );
01458         }
01459 
01468         protected function doPrimeContainerCache( array $containerInfo ) {}
01469 
01476         private function fileCacheKey( $path ) {
01477                 return wfMemcKey( 'backend', $this->getName(), 'file', sha1( $path ) );
01478         }
01479 
01488         final protected function setFileCache( $path, $val ) {
01489                 $this->memCache->add( $this->fileCacheKey( $path ), $val, 7*86400 );
01490         }
01491 
01498         final protected function deleteFileCache( $path ) {
01499                 if ( !$this->memCache->set( $this->fileCacheKey( $path ), 'PURGED', 300 ) ) {
01500                         trigger_error( "Unable to delete stat cache for file $path." );
01501                 }
01502         }
01503 
01511         final protected function primeFileCache( array $items ) {
01512                 wfProfileIn( __METHOD__ );
01513                 wfProfileIn( __METHOD__ . '-' . $this->name );
01514 
01515                 $paths = array(); // list of storage paths
01516                 $pathNames = array(); // (cache key => storage path)
01517                 // Get all the paths/containers from the items...
01518                 foreach ( $items as $item ) {
01519                         if ( $item instanceof FileOp ) {
01520                                 $paths = array_merge( $paths, $item->storagePathsRead() );
01521                                 $paths = array_merge( $paths, $item->storagePathsChanged() );
01522                         } elseif ( self::isStoragePath( $item ) ) {
01523                                 $paths[] = $item;
01524                         }
01525                 }
01526                 // Get all the corresponding cache keys for paths...
01527                 foreach ( $paths as $path ) {
01528                         list( $cont, $rel, $s ) = $this->resolveStoragePath( $path );
01529                         if ( $rel !== null ) { // valid path for this backend
01530                                 $pathNames[$this->fileCacheKey( $path )] = $path;
01531                         }
01532                 }
01533                 // Get all cache entries for these container cache keys...
01534                 $values = $this->memCache->getMulti( array_keys( $pathNames ) );
01535                 foreach ( $values as $cacheKey => $val ) {
01536                         if ( is_array( $val ) ) {
01537                                 $path = $pathNames[$cacheKey];
01538                                 $this->cheapCache->set( $path, 'stat', $val );
01539                                 if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
01540                                         $this->cheapCache->set( $path, 'sha1',
01541                                                 array( 'hash' => $val['sha1'], 'latest' => $val['latest'] ) );
01542                                 }
01543                         }
01544                 }
01545 
01546                 wfProfileOut( __METHOD__ . '-' . $this->name );
01547                 wfProfileOut( __METHOD__ );
01548         }
01549 }
01550 
01559 abstract class FileBackendStoreOpHandle {
01561         public $params = array(); // params to caller functions
01563         public $backend;
01565         public $resourcesToClose = array();
01566 
01567         public $call; // string; name that identifies the function called
01568 
01574         public function closeResources() {
01575                 array_map( 'fclose', $this->resourcesToClose );
01576         }
01577 }
01578 
01585 abstract class FileBackendStoreShardListIterator implements Iterator {
01587         protected $backend;
01589         protected $params;
01591         protected $shardSuffixes;
01592         protected $container; // string; full container name
01593         protected $directory; // string; resolved relative path
01594 
01596         protected $iter;
01597         protected $curShard = 0; // integer
01598         protected $pos = 0; // integer
01599 
01601         protected $multiShardPaths = array(); // (rel path => 1)
01602 
01610         public function __construct(
01611                 FileBackendStore $backend, $container, $dir, array $suffixes, array $params
01612         ) {
01613                 $this->backend = $backend;
01614                 $this->container = $container;
01615                 $this->directory = $dir;
01616                 $this->shardSuffixes = $suffixes;
01617                 $this->params = $params;
01618         }
01619 
01624         public function key() {
01625                 return $this->pos;
01626         }
01627 
01632         public function valid() {
01633                 if ( $this->iter instanceof Iterator ) {
01634                         return $this->iter->valid();
01635                 } elseif ( is_array( $this->iter ) ) {
01636                         return ( current( $this->iter ) !== false ); // no paths can have this value
01637                 }
01638                 return false; // some failure?
01639         }
01640 
01645         public function current() {
01646                 return ( $this->iter instanceof Iterator )
01647                         ? $this->iter->current()
01648                         : current( $this->iter );
01649         }
01650 
01655         public function next() {
01656                 ++$this->pos;
01657                 ( $this->iter instanceof Iterator ) ? $this->iter->next() : next( $this->iter );
01658                 do {
01659                         $continue = false; // keep scanning shards?
01660                         $this->filterViaNext(); // filter out duplicates
01661                         // Find the next non-empty shard if no elements are left
01662                         if ( !$this->valid() ) {
01663                                 $this->nextShardIteratorIfNotValid();
01664                                 $continue = $this->valid(); // re-filter unless we ran out of shards
01665                         }
01666                 } while ( $continue );
01667         }
01668 
01673         public function rewind() {
01674                 $this->pos = 0;
01675                 $this->curShard = 0;
01676                 $this->setIteratorFromCurrentShard();
01677                 do {
01678                         $continue = false; // keep scanning shards?
01679                         $this->filterViaNext(); // filter out duplicates
01680                         // Find the next non-empty shard if no elements are left
01681                         if ( !$this->valid() ) {
01682                                 $this->nextShardIteratorIfNotValid();
01683                                 $continue = $this->valid(); // re-filter unless we ran out of shards
01684                         }
01685                 } while ( $continue );
01686         }
01687 
01691         protected function filterViaNext() {
01692                 while ( $this->valid() ) {
01693                         $rel = $this->iter->current(); // path relative to given directory
01694                         $path = $this->params['dir'] . "/{$rel}"; // full storage path
01695                         if ( $this->backend->isSingleShardPathInternal( $path ) ) {
01696                                 break; // path is only on one shard; no issue with duplicates
01697                         } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
01698                                 // Don't keep listing paths that are on multiple shards
01699                                 ( $this->iter instanceof Iterator ) ? $this->iter->next() : next( $this->iter );
01700                         } else {
01701                                 $this->multiShardPaths[$rel] = 1;
01702                                 break;
01703                         }
01704                 }
01705         }
01706 
01712         protected function nextShardIteratorIfNotValid() {
01713                 while ( !$this->valid() && ++$this->curShard < count( $this->shardSuffixes ) ) {
01714                         $this->setIteratorFromCurrentShard();
01715                 }
01716         }
01717 
01721         protected function setIteratorFromCurrentShard() {
01722                 $this->iter = $this->listFromShard(
01723                         $this->container . $this->shardSuffixes[$this->curShard],
01724                         $this->directory, $this->params );
01725                 // Start loading results so that current() works
01726                 if ( $this->iter ) {
01727                         ( $this->iter instanceof Iterator ) ? $this->iter->rewind() : reset( $this->iter );
01728                 }
01729         }
01730 
01739         abstract protected function listFromShard( $container, $dir, array $params );
01740 }
01741 
01745 class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
01750         protected function listFromShard( $container, $dir, array $params ) {
01751                 return $this->backend->getDirectoryListInternal( $container, $dir, $params );
01752         }
01753 }
01754 
01758 class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
01763         protected function listFromShard( $container, $dir, array $params ) {
01764                 return $this->backend->getFileListInternal( $container, $dir, $params );
01765         }
01766 }