MediaWiki  REL1_24
FileBackendStore.php
Go to the documentation of this file.
00001 <?php
00038 abstract class FileBackendStore extends FileBackend {
00040     protected $memCache;
00042     protected $cheapCache;
00044     protected $expensiveCache;
00045 
00047     protected $shardViaHashLevels = array();
00048 
00050     protected $mimeCallback;
00051 
00052     protected $maxFileSize = 4294967296; // integer bytes (4GiB)
00053 
00054     const CACHE_TTL = 10; // integer; TTL in seconds for process cache entries
00055     const CACHE_CHEAP_SIZE = 500; // integer; max entries in "cheap cache"
00056     const CACHE_EXPENSIVE_SIZE = 5; // integer; max entries in "expensive cache"
00057 
00067     public function __construct( array $config ) {
00068         parent::__construct( $config );
00069         $this->mimeCallback = isset( $config['mimeCallback'] )
00070             ? $config['mimeCallback']
00071             : function ( $storagePath, $content, $fsPath ) {
00072                 // @todo handle the case of extension-less files using the contents
00073                 return StreamFile::contentTypeFromPath( $storagePath ) ?: 'unknown/unknown';
00074             };
00075         $this->memCache = new EmptyBagOStuff(); // disabled by default
00076         $this->cheapCache = new ProcessCacheLRU( self::CACHE_CHEAP_SIZE );
00077         $this->expensiveCache = new ProcessCacheLRU( self::CACHE_EXPENSIVE_SIZE );
00078     }
00079 
00087     final public function maxFileSizeInternal() {
00088         return $this->maxFileSize;
00089     }
00090 
00100     abstract public function isPathUsableInternal( $storagePath );
00101 
00120     final public function createInternal( array $params ) {
00121         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00122         if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
00123             $status = Status::newFatal( 'backend-fail-maxsize',
00124                 $params['dst'], $this->maxFileSizeInternal() );
00125         } else {
00126             $status = $this->doCreateInternal( $params );
00127             $this->clearCache( array( $params['dst'] ) );
00128             if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
00129                 $this->deleteFileCache( $params['dst'] ); // persistent cache
00130             }
00131         }
00132 
00133         return $status;
00134     }
00135 
00141     abstract protected function doCreateInternal( array $params );
00142 
00161     final public function storeInternal( array $params ) {
00162         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00163         if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
00164             $status = Status::newFatal( 'backend-fail-maxsize',
00165                 $params['dst'], $this->maxFileSizeInternal() );
00166         } else {
00167             $status = $this->doStoreInternal( $params );
00168             $this->clearCache( array( $params['dst'] ) );
00169             if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
00170                 $this->deleteFileCache( $params['dst'] ); // persistent cache
00171             }
00172         }
00173 
00174         return $status;
00175     }
00176 
00182     abstract protected function doStoreInternal( array $params );
00183 
00203     final public function copyInternal( array $params ) {
00204         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00205         $status = $this->doCopyInternal( $params );
00206         $this->clearCache( array( $params['dst'] ) );
00207         if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
00208             $this->deleteFileCache( $params['dst'] ); // persistent cache
00209         }
00210 
00211         return $status;
00212     }
00213 
00219     abstract protected function doCopyInternal( array $params );
00220 
00235     final public function deleteInternal( array $params ) {
00236         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00237         $status = $this->doDeleteInternal( $params );
00238         $this->clearCache( array( $params['src'] ) );
00239         $this->deleteFileCache( $params['src'] ); // persistent cache
00240         return $status;
00241     }
00242 
00248     abstract protected function doDeleteInternal( array $params );
00249 
00269     final public function moveInternal( array $params ) {
00270         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00271         $status = $this->doMoveInternal( $params );
00272         $this->clearCache( array( $params['src'], $params['dst'] ) );
00273         $this->deleteFileCache( $params['src'] ); // persistent cache
00274         if ( !isset( $params['dstExists'] ) || $params['dstExists'] ) {
00275             $this->deleteFileCache( $params['dst'] ); // persistent cache
00276         }
00277 
00278         return $status;
00279     }
00280 
00286     protected function doMoveInternal( array $params ) {
00287         unset( $params['async'] ); // two steps, won't work here :)
00288         $nsrc = FileBackend::normalizeStoragePath( $params['src'] );
00289         $ndst = FileBackend::normalizeStoragePath( $params['dst'] );
00290         // Copy source to dest
00291         $status = $this->copyInternal( $params );
00292         if ( $nsrc !== $ndst && $status->isOK() ) {
00293             // Delete source (only fails due to races or network problems)
00294             $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) );
00295             $status->setResult( true, $status->value ); // ignore delete() errors
00296         }
00297 
00298         return $status;
00299     }
00300 
00315     final public function describeInternal( array $params ) {
00316         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00317         if ( count( $params['headers'] ) ) {
00318             $status = $this->doDescribeInternal( $params );
00319             $this->clearCache( array( $params['src'] ) );
00320             $this->deleteFileCache( $params['src'] ); // persistent cache
00321         } else {
00322             $status = Status::newGood(); // nothing to do
00323         }
00324 
00325         return $status;
00326     }
00327 
00333     protected function doDescribeInternal( array $params ) {
00334         return Status::newGood();
00335     }
00336 
00344     final public function nullInternal( array $params ) {
00345         return Status::newGood();
00346     }
00347 
00348     final public function concatenate( array $params ) {
00349         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00350         $status = Status::newGood();
00351 
00352         // Try to lock the source files for the scope of this function
00353         $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
00354         if ( $status->isOK() ) {
00355             // Actually do the file concatenation...
00356             $start_time = microtime( true );
00357             $status->merge( $this->doConcatenate( $params ) );
00358             $sec = microtime( true ) - $start_time;
00359             if ( !$status->isOK() ) {
00360                 wfDebugLog( 'FileOperation', get_class( $this ) . "-{$this->name}" .
00361                     " failed to concatenate " . count( $params['srcs'] ) . " file(s) [$sec sec]" );
00362             }
00363         }
00364 
00365         return $status;
00366     }
00367 
00373     protected function doConcatenate( array $params ) {
00374         $status = Status::newGood();
00375         $tmpPath = $params['dst']; // convenience
00376         unset( $params['latest'] ); // sanity
00377 
00378         // Check that the specified temp file is valid...
00379         wfSuppressWarnings();
00380         $ok = ( is_file( $tmpPath ) && filesize( $tmpPath ) == 0 );
00381         wfRestoreWarnings();
00382         if ( !$ok ) { // not present or not empty
00383             $status->fatal( 'backend-fail-opentemp', $tmpPath );
00384 
00385             return $status;
00386         }
00387 
00388         // Get local FS versions of the chunks needed for the concatenation...
00389         $fsFiles = $this->getLocalReferenceMulti( $params );
00390         foreach ( $fsFiles as $path => &$fsFile ) {
00391             if ( !$fsFile ) { // chunk failed to download?
00392                 $fsFile = $this->getLocalReference( array( 'src' => $path ) );
00393                 if ( !$fsFile ) { // retry failed?
00394                     $status->fatal( 'backend-fail-read', $path );
00395 
00396                     return $status;
00397                 }
00398             }
00399         }
00400         unset( $fsFile ); // unset reference so we can reuse $fsFile
00401 
00402         // Get a handle for the destination temp file
00403         $tmpHandle = fopen( $tmpPath, 'ab' );
00404         if ( $tmpHandle === false ) {
00405             $status->fatal( 'backend-fail-opentemp', $tmpPath );
00406 
00407             return $status;
00408         }
00409 
00410         // Build up the temp file using the source chunks (in order)...
00411         foreach ( $fsFiles as $virtualSource => $fsFile ) {
00412             // Get a handle to the local FS version
00413             $sourceHandle = fopen( $fsFile->getPath(), 'rb' );
00414             if ( $sourceHandle === false ) {
00415                 fclose( $tmpHandle );
00416                 $status->fatal( 'backend-fail-read', $virtualSource );
00417 
00418                 return $status;
00419             }
00420             // Append chunk to file (pass chunk size to avoid magic quotes)
00421             if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
00422                 fclose( $sourceHandle );
00423                 fclose( $tmpHandle );
00424                 $status->fatal( 'backend-fail-writetemp', $tmpPath );
00425 
00426                 return $status;
00427             }
00428             fclose( $sourceHandle );
00429         }
00430         if ( !fclose( $tmpHandle ) ) {
00431             $status->fatal( 'backend-fail-closetemp', $tmpPath );
00432 
00433             return $status;
00434         }
00435 
00436         clearstatcache(); // temp file changed
00437 
00438         return $status;
00439     }
00440 
00441     final protected function doPrepare( array $params ) {
00442         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00443         $status = Status::newGood();
00444 
00445         list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00446         if ( $dir === null ) {
00447             $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
00448 
00449             return $status; // invalid storage path
00450         }
00451 
00452         if ( $shard !== null ) { // confined to a single container/shard
00453             $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
00454         } else { // directory is on several shards
00455             wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00456             list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
00457             foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
00458                 $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
00459             }
00460         }
00461 
00462         return $status;
00463     }
00464 
00472     protected function doPrepareInternal( $container, $dir, array $params ) {
00473         return Status::newGood();
00474     }
00475 
00476     final protected function doSecure( array $params ) {
00477         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00478         $status = Status::newGood();
00479 
00480         list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00481         if ( $dir === null ) {
00482             $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
00483 
00484             return $status; // invalid storage path
00485         }
00486 
00487         if ( $shard !== null ) { // confined to a single container/shard
00488             $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
00489         } else { // directory is on several shards
00490             wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00491             list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
00492             foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
00493                 $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
00494             }
00495         }
00496 
00497         return $status;
00498     }
00499 
00507     protected function doSecureInternal( $container, $dir, array $params ) {
00508         return Status::newGood();
00509     }
00510 
00511     final protected function doPublish( array $params ) {
00512         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00513         $status = Status::newGood();
00514 
00515         list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00516         if ( $dir === null ) {
00517             $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
00518 
00519             return $status; // invalid storage path
00520         }
00521 
00522         if ( $shard !== null ) { // confined to a single container/shard
00523             $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
00524         } else { // directory is on several shards
00525             wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00526             list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
00527             foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
00528                 $status->merge( $this->doPublishInternal( "{$fullCont}{$suffix}", $dir, $params ) );
00529             }
00530         }
00531 
00532         return $status;
00533     }
00534 
00542     protected function doPublishInternal( $container, $dir, array $params ) {
00543         return Status::newGood();
00544     }
00545 
00546     final protected function doClean( array $params ) {
00547         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00548         $status = Status::newGood();
00549 
00550         // Recursive: first delete all empty subdirs recursively
00551         if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
00552             $subDirsRel = $this->getTopDirectoryList( array( 'dir' => $params['dir'] ) );
00553             if ( $subDirsRel !== null ) { // no errors
00554                 foreach ( $subDirsRel as $subDirRel ) {
00555                     $subDir = $params['dir'] . "/{$subDirRel}"; // full path
00556                     $status->merge( $this->doClean( array( 'dir' => $subDir ) + $params ) );
00557                 }
00558                 unset( $subDirsRel ); // free directory for rmdir() on Windows (for FS backends)
00559             }
00560         }
00561 
00562         list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00563         if ( $dir === null ) {
00564             $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
00565 
00566             return $status; // invalid storage path
00567         }
00568 
00569         // Attempt to lock this directory...
00570         $filesLockEx = array( $params['dir'] );
00571         $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
00572         if ( !$status->isOK() ) {
00573             return $status; // abort
00574         }
00575 
00576         if ( $shard !== null ) { // confined to a single container/shard
00577             $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
00578             $this->deleteContainerCache( $fullCont ); // purge cache
00579         } else { // directory is on several shards
00580             wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00581             list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
00582             foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
00583                 $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
00584                 $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
00585             }
00586         }
00587 
00588         return $status;
00589     }
00590 
00598     protected function doCleanInternal( $container, $dir, array $params ) {
00599         return Status::newGood();
00600     }
00601 
00602     final public function fileExists( array $params ) {
00603         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00604         $stat = $this->getFileStat( $params );
00605 
00606         return ( $stat === null ) ? null : (bool)$stat; // null => failure
00607     }
00608 
00609     final public function getFileTimestamp( array $params ) {
00610         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00611         $stat = $this->getFileStat( $params );
00612 
00613         return $stat ? $stat['mtime'] : false;
00614     }
00615 
00616     final public function getFileSize( array $params ) {
00617         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00618         $stat = $this->getFileStat( $params );
00619 
00620         return $stat ? $stat['size'] : false;
00621     }
00622 
00623     final public function getFileStat( array $params ) {
00624         $path = self::normalizeStoragePath( $params['src'] );
00625         if ( $path === null ) {
00626             return false; // invalid storage path
00627         }
00628         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00629         $latest = !empty( $params['latest'] ); // use latest data?
00630         if ( !$this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
00631             $this->primeFileCache( array( $path ) ); // check persistent cache
00632         }
00633         if ( $this->cheapCache->has( $path, 'stat', self::CACHE_TTL ) ) {
00634             $stat = $this->cheapCache->get( $path, 'stat' );
00635             // If we want the latest data, check that this cached
00636             // value was in fact fetched with the latest available data.
00637             if ( is_array( $stat ) ) {
00638                 if ( !$latest || $stat['latest'] ) {
00639                     return $stat;
00640                 }
00641             } elseif ( in_array( $stat, array( 'NOT_EXIST', 'NOT_EXIST_LATEST' ) ) ) {
00642                 if ( !$latest || $stat === 'NOT_EXIST_LATEST' ) {
00643                     return false;
00644                 }
00645             }
00646         }
00647         wfProfileIn( __METHOD__ . '-miss-' . $this->name );
00648         $stat = $this->doGetFileStat( $params );
00649         wfProfileOut( __METHOD__ . '-miss-' . $this->name );
00650         if ( is_array( $stat ) ) { // file exists
00651             // Strongly consistent backends can automatically set "latest"
00652             $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
00653             $this->cheapCache->set( $path, 'stat', $stat );
00654             $this->setFileCache( $path, $stat ); // update persistent cache
00655             if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
00656                 $this->cheapCache->set( $path, 'sha1',
00657                     array( 'hash' => $stat['sha1'], 'latest' => $latest ) );
00658             }
00659             if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
00660                 $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
00661                 $this->cheapCache->set( $path, 'xattr',
00662                     array( 'map' => $stat['xattr'], 'latest' => $latest ) );
00663             }
00664         } elseif ( $stat === false ) { // file does not exist
00665             $this->cheapCache->set( $path, 'stat', $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
00666             $this->cheapCache->set( $path, 'xattr', array( 'map' => false, 'latest' => $latest ) );
00667             $this->cheapCache->set( $path, 'sha1', array( 'hash' => false, 'latest' => $latest ) );
00668             wfDebug( __METHOD__ . ": File $path does not exist.\n" );
00669         } else { // an error occurred
00670             wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
00671         }
00672 
00673         return $stat;
00674     }
00675 
00679     abstract protected function doGetFileStat( array $params );
00680 
00681     public function getFileContentsMulti( array $params ) {
00682         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00683 
00684         $params = $this->setConcurrencyFlags( $params );
00685         $contents = $this->doGetFileContentsMulti( $params );
00686 
00687         return $contents;
00688     }
00689 
00695     protected function doGetFileContentsMulti( array $params ) {
00696         $contents = array();
00697         foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
00698             wfSuppressWarnings();
00699             $contents[$path] = $fsFile ? file_get_contents( $fsFile->getPath() ) : false;
00700             wfRestoreWarnings();
00701         }
00702 
00703         return $contents;
00704     }
00705 
00706     final public function getFileXAttributes( array $params ) {
00707         $path = self::normalizeStoragePath( $params['src'] );
00708         if ( $path === null ) {
00709             return false; // invalid storage path
00710         }
00711         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00712         $latest = !empty( $params['latest'] ); // use latest data?
00713         if ( $this->cheapCache->has( $path, 'xattr', self::CACHE_TTL ) ) {
00714             $stat = $this->cheapCache->get( $path, 'xattr' );
00715             // If we want the latest data, check that this cached
00716             // value was in fact fetched with the latest available data.
00717             if ( !$latest || $stat['latest'] ) {
00718                 return $stat['map'];
00719             }
00720         }
00721         wfProfileIn( __METHOD__ . '-miss' );
00722         wfProfileIn( __METHOD__ . '-miss-' . $this->name );
00723         $fields = $this->doGetFileXAttributes( $params );
00724         $fields = is_array( $fields ) ? self::normalizeXAttributes( $fields ) : false;
00725         wfProfileOut( __METHOD__ . '-miss-' . $this->name );
00726         wfProfileOut( __METHOD__ . '-miss' );
00727         $this->cheapCache->set( $path, 'xattr', array( 'map' => $fields, 'latest' => $latest ) );
00728 
00729         return $fields;
00730     }
00731 
00736     protected function doGetFileXAttributes( array $params ) {
00737         return array( 'headers' => array(), 'metadata' => array() ); // not supported
00738     }
00739 
00740     final public function getFileSha1Base36( array $params ) {
00741         $path = self::normalizeStoragePath( $params['src'] );
00742         if ( $path === null ) {
00743             return false; // invalid storage path
00744         }
00745         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00746         $latest = !empty( $params['latest'] ); // use latest data?
00747         if ( $this->cheapCache->has( $path, 'sha1', self::CACHE_TTL ) ) {
00748             $stat = $this->cheapCache->get( $path, 'sha1' );
00749             // If we want the latest data, check that this cached
00750             // value was in fact fetched with the latest available data.
00751             if ( !$latest || $stat['latest'] ) {
00752                 return $stat['hash'];
00753             }
00754         }
00755         wfProfileIn( __METHOD__ . '-miss-' . $this->name );
00756         $hash = $this->doGetFileSha1Base36( $params );
00757         wfProfileOut( __METHOD__ . '-miss-' . $this->name );
00758         $this->cheapCache->set( $path, 'sha1', array( 'hash' => $hash, 'latest' => $latest ) );
00759 
00760         return $hash;
00761     }
00762 
00768     protected function doGetFileSha1Base36( array $params ) {
00769         $fsFile = $this->getLocalReference( $params );
00770         if ( !$fsFile ) {
00771             return false;
00772         } else {
00773             return $fsFile->getSha1Base36();
00774         }
00775     }
00776 
00777     final public function getFileProps( array $params ) {
00778         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00779         $fsFile = $this->getLocalReference( $params );
00780         $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
00781 
00782         return $props;
00783     }
00784 
00785     final public function getLocalReferenceMulti( array $params ) {
00786         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00787 
00788         $params = $this->setConcurrencyFlags( $params );
00789 
00790         $fsFiles = array(); // (path => FSFile)
00791         $latest = !empty( $params['latest'] ); // use latest data?
00792         // Reuse any files already in process cache...
00793         foreach ( $params['srcs'] as $src ) {
00794             $path = self::normalizeStoragePath( $src );
00795             if ( $path === null ) {
00796                 $fsFiles[$src] = null; // invalid storage path
00797             } elseif ( $this->expensiveCache->has( $path, 'localRef' ) ) {
00798                 $val = $this->expensiveCache->get( $path, 'localRef' );
00799                 // If we want the latest data, check that this cached
00800                 // value was in fact fetched with the latest available data.
00801                 if ( !$latest || $val['latest'] ) {
00802                     $fsFiles[$src] = $val['object'];
00803                 }
00804             }
00805         }
00806         // Fetch local references of any remaning files...
00807         $params['srcs'] = array_diff( $params['srcs'], array_keys( $fsFiles ) );
00808         foreach ( $this->doGetLocalReferenceMulti( $params ) as $path => $fsFile ) {
00809             $fsFiles[$path] = $fsFile;
00810             if ( $fsFile ) { // update the process cache...
00811                 $this->expensiveCache->set( $path, 'localRef',
00812                     array( 'object' => $fsFile, 'latest' => $latest ) );
00813             }
00814         }
00815 
00816         return $fsFiles;
00817     }
00818 
00824     protected function doGetLocalReferenceMulti( array $params ) {
00825         return $this->doGetLocalCopyMulti( $params );
00826     }
00827 
00828     final public function getLocalCopyMulti( array $params ) {
00829         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00830 
00831         $params = $this->setConcurrencyFlags( $params );
00832         $tmpFiles = $this->doGetLocalCopyMulti( $params );
00833 
00834         return $tmpFiles;
00835     }
00836 
00842     abstract protected function doGetLocalCopyMulti( array $params );
00843 
00849     public function getFileHttpUrl( array $params ) {
00850         return null; // not supported
00851     }
00852 
00853     final public function streamFile( array $params ) {
00854         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
00855         $status = Status::newGood();
00856 
00857         $info = $this->getFileStat( $params );
00858         if ( !$info ) { // let StreamFile handle the 404
00859             $status->fatal( 'backend-fail-notexists', $params['src'] );
00860         }
00861 
00862         // Set output buffer and HTTP headers for stream
00863         $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : array();
00864         $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
00865         if ( $res == StreamFile::NOT_MODIFIED ) {
00866             // do nothing; client cache is up to date
00867         } elseif ( $res == StreamFile::READY_STREAM ) {
00868             wfProfileIn( __METHOD__ . '-send-' . $this->name );
00869             $status = $this->doStreamFile( $params );
00870             wfProfileOut( __METHOD__ . '-send-' . $this->name );
00871             if ( !$status->isOK() ) {
00872                 // Per bug 41113, nasty things can happen if bad cache entries get
00873                 // stuck in cache. It's also possible that this error can come up
00874                 // with simple race conditions. Clear out the stat cache to be safe.
00875                 $this->clearCache( array( $params['src'] ) );
00876                 $this->deleteFileCache( $params['src'] );
00877                 trigger_error( "Bad stat cache or race condition for file {$params['src']}." );
00878             }
00879         } else {
00880             $status->fatal( 'backend-fail-stream', $params['src'] );
00881         }
00882 
00883         return $status;
00884     }
00885 
00891     protected function doStreamFile( array $params ) {
00892         $status = Status::newGood();
00893 
00894         $fsFile = $this->getLocalReference( $params );
00895         if ( !$fsFile ) {
00896             $status->fatal( 'backend-fail-stream', $params['src'] );
00897         } elseif ( !readfile( $fsFile->getPath() ) ) {
00898             $status->fatal( 'backend-fail-stream', $params['src'] );
00899         }
00900 
00901         return $status;
00902     }
00903 
00904     final public function directoryExists( array $params ) {
00905         list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00906         if ( $dir === null ) {
00907             return false; // invalid storage path
00908         }
00909         if ( $shard !== null ) { // confined to a single container/shard
00910             return $this->doDirectoryExists( $fullCont, $dir, $params );
00911         } else { // directory is on several shards
00912             wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00913             list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
00914             $res = false; // response
00915             foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
00916                 $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
00917                 if ( $exists ) {
00918                     $res = true;
00919                     break; // found one!
00920                 } elseif ( $exists === null ) { // error?
00921                     $res = null; // if we don't find anything, it is indeterminate
00922                 }
00923             }
00924 
00925             return $res;
00926         }
00927     }
00928 
00937     abstract protected function doDirectoryExists( $container, $dir, array $params );
00938 
00939     final public function getDirectoryList( array $params ) {
00940         list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00941         if ( $dir === null ) { // invalid storage path
00942             return null;
00943         }
00944         if ( $shard !== null ) {
00945             // File listing is confined to a single container/shard
00946             return $this->getDirectoryListInternal( $fullCont, $dir, $params );
00947         } else {
00948             wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00949             // File listing spans multiple containers/shards
00950             list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
00951 
00952             return new FileBackendStoreShardDirIterator( $this,
00953                 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
00954         }
00955     }
00956 
00967     abstract public function getDirectoryListInternal( $container, $dir, array $params );
00968 
00969     final public function getFileList( array $params ) {
00970         list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
00971         if ( $dir === null ) { // invalid storage path
00972             return null;
00973         }
00974         if ( $shard !== null ) {
00975             // File listing is confined to a single container/shard
00976             return $this->getFileListInternal( $fullCont, $dir, $params );
00977         } else {
00978             wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
00979             // File listing spans multiple containers/shards
00980             list( , $shortCont, ) = self::splitStoragePath( $params['dir'] );
00981 
00982             return new FileBackendStoreShardFileIterator( $this,
00983                 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
00984         }
00985     }
00986 
00997     abstract public function getFileListInternal( $container, $dir, array $params );
00998 
01010     final public function getOperationsInternal( array $ops ) {
01011         $supportedOps = array(
01012             'store' => 'StoreFileOp',
01013             'copy' => 'CopyFileOp',
01014             'move' => 'MoveFileOp',
01015             'delete' => 'DeleteFileOp',
01016             'create' => 'CreateFileOp',
01017             'describe' => 'DescribeFileOp',
01018             'null' => 'NullFileOp'
01019         );
01020 
01021         $performOps = array(); // array of FileOp objects
01022         // Build up ordered array of FileOps...
01023         foreach ( $ops as $operation ) {
01024             $opName = $operation['op'];
01025             if ( isset( $supportedOps[$opName] ) ) {
01026                 $class = $supportedOps[$opName];
01027                 // Get params for this operation
01028                 $params = $operation;
01029                 // Append the FileOp class
01030                 $performOps[] = new $class( $this, $params );
01031             } else {
01032                 throw new FileBackendError( "Operation '$opName' is not supported." );
01033             }
01034         }
01035 
01036         return $performOps;
01037     }
01038 
01049     final public function getPathsToLockForOpsInternal( array $performOps ) {
01050         // Build up a list of files to lock...
01051         $paths = array( 'sh' => array(), 'ex' => array() );
01052         foreach ( $performOps as $fileOp ) {
01053             $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
01054             $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
01055         }
01056         // Optimization: if doing an EX lock anyway, don't also set an SH one
01057         $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
01058         // Get a shared lock on the parent directory of each path changed
01059         $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
01060 
01061         return array(
01062             LockManager::LOCK_UW => $paths['sh'],
01063             LockManager::LOCK_EX => $paths['ex']
01064         );
01065     }
01066 
01067     public function getScopedLocksForOps( array $ops, Status $status ) {
01068         $paths = $this->getPathsToLockForOpsInternal( $this->getOperationsInternal( $ops ) );
01069 
01070         return array( $this->getScopedFileLocks( $paths, 'mixed', $status ) );
01071     }
01072 
01073     final protected function doOperationsInternal( array $ops, array $opts ) {
01074         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01075         $status = Status::newGood();
01076 
01077         // Fix up custom header name/value pairs...
01078         $ops = array_map( array( $this, 'stripInvalidHeadersFromOp' ), $ops );
01079 
01080         // Build up a list of FileOps...
01081         $performOps = $this->getOperationsInternal( $ops );
01082 
01083         // Acquire any locks as needed...
01084         if ( empty( $opts['nonLocking'] ) ) {
01085             // Build up a list of files to lock...
01086             $paths = $this->getPathsToLockForOpsInternal( $performOps );
01087             // Try to lock those files for the scope of this function...
01088             $scopeLock = $this->getScopedFileLocks( $paths, 'mixed', $status );
01089             if ( !$status->isOK() ) {
01090                 return $status; // abort
01091             }
01092         }
01093 
01094         // Clear any file cache entries (after locks acquired)
01095         if ( empty( $opts['preserveCache'] ) ) {
01096             $this->clearCache();
01097         }
01098 
01099         // Build the list of paths involved
01100         $paths = array();
01101         foreach ( $performOps as $op ) {
01102             $paths = array_merge( $paths, $op->storagePathsRead() );
01103             $paths = array_merge( $paths, $op->storagePathsChanged() );
01104         }
01105 
01106         // Enlarge the cache to fit the stat entries of these files
01107         $this->cheapCache->resize( max( 2 * count( $paths ), self::CACHE_CHEAP_SIZE ) );
01108 
01109         // Load from the persistent container caches
01110         $this->primeContainerCache( $paths );
01111         // Get the latest stat info for all the files (having locked them)
01112         $ok = $this->preloadFileStat( array( 'srcs' => $paths, 'latest' => true ) );
01113 
01114         if ( $ok ) {
01115             // Actually attempt the operation batch...
01116             $opts = $this->setConcurrencyFlags( $opts );
01117             $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
01118         } else {
01119             // If we could not even stat some files, then bail out...
01120             $subStatus = Status::newFatal( 'backend-fail-internal', $this->name );
01121             foreach ( $ops as $i => $op ) { // mark each op as failed
01122                 $subStatus->success[$i] = false;
01123                 ++$subStatus->failCount;
01124             }
01125             wfDebugLog( 'FileOperation', get_class( $this ) . "-{$this->name} " .
01126                 " stat failure; aborted operations: " . FormatJson::encode( $ops ) );
01127         }
01128 
01129         // Merge errors into status fields
01130         $status->merge( $subStatus );
01131         $status->success = $subStatus->success; // not done in merge()
01132 
01133         // Shrink the stat cache back to normal size
01134         $this->cheapCache->resize( self::CACHE_CHEAP_SIZE );
01135 
01136         return $status;
01137     }
01138 
01139     final protected function doQuickOperationsInternal( array $ops ) {
01140         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01141         $status = Status::newGood();
01142 
01143         // Fix up custom header name/value pairs...
01144         $ops = array_map( array( $this, 'stripInvalidHeadersFromOp' ), $ops );
01145 
01146         // Clear any file cache entries
01147         $this->clearCache();
01148 
01149         $supportedOps = array( 'create', 'store', 'copy', 'move', 'delete', 'describe', 'null' );
01150         // Parallel ops may be disabled in config due to dependencies (e.g. needing popen())
01151         $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
01152         $maxConcurrency = $this->concurrency; // throttle
01153 
01154         $statuses = array(); // array of (index => Status)
01155         $fileOpHandles = array(); // list of (index => handle) arrays
01156         $curFileOpHandles = array(); // current handle batch
01157         // Perform the sync-only ops and build up op handles for the async ops...
01158         foreach ( $ops as $index => $params ) {
01159             if ( !in_array( $params['op'], $supportedOps ) ) {
01160                 throw new FileBackendError( "Operation '{$params['op']}' is not supported." );
01161             }
01162             $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
01163             $subStatus = $this->$method( array( 'async' => $async ) + $params );
01164             if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
01165                 if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
01166                     $fileOpHandles[] = $curFileOpHandles; // push this batch
01167                     $curFileOpHandles = array();
01168                 }
01169                 $curFileOpHandles[$index] = $subStatus->value; // keep index
01170             } else { // error or completed
01171                 $statuses[$index] = $subStatus; // keep index
01172             }
01173         }
01174         if ( count( $curFileOpHandles ) ) {
01175             $fileOpHandles[] = $curFileOpHandles; // last batch
01176         }
01177         // Do all the async ops that can be done concurrently...
01178         foreach ( $fileOpHandles as $fileHandleBatch ) {
01179             $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
01180         }
01181         // Marshall and merge all the responses...
01182         foreach ( $statuses as $index => $subStatus ) {
01183             $status->merge( $subStatus );
01184             if ( $subStatus->isOK() ) {
01185                 $status->success[$index] = true;
01186                 ++$status->successCount;
01187             } else {
01188                 $status->success[$index] = false;
01189                 ++$status->failCount;
01190             }
01191         }
01192 
01193         return $status;
01194     }
01195 
01206     final public function executeOpHandlesInternal( array $fileOpHandles ) {
01207         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01208 
01209         foreach ( $fileOpHandles as $fileOpHandle ) {
01210             if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
01211                 throw new FileBackendError( "Given a non-FileBackendStoreOpHandle object." );
01212             } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
01213                 throw new FileBackendError( "Given a FileBackendStoreOpHandle for the wrong backend." );
01214             }
01215         }
01216         $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
01217         foreach ( $fileOpHandles as $fileOpHandle ) {
01218             $fileOpHandle->closeResources();
01219         }
01220 
01221         return $res;
01222     }
01223 
01230     protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
01231         if ( count( $fileOpHandles ) ) {
01232             throw new FileBackendError( "This backend supports no asynchronous operations." );
01233         }
01234 
01235         return array();
01236     }
01237 
01247     protected function stripInvalidHeadersFromOp( array $op ) {
01248         static $longs = array( 'Content-Disposition' );
01249         if ( isset( $op['headers'] ) ) { // op sets HTTP headers
01250             foreach ( $op['headers'] as $name => $value ) {
01251                 $maxHVLen = in_array( $name, $longs ) ? INF : 255;
01252                 if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
01253                     trigger_error( "Header '$name: $value' is too long." );
01254                     unset( $op['headers'][$name] );
01255                 } elseif ( !strlen( $value ) ) {
01256                     $op['headers'][$name] = ''; // null/false => ""
01257                 }
01258             }
01259         }
01260 
01261         return $op;
01262     }
01263 
01264     final public function preloadCache( array $paths ) {
01265         $fullConts = array(); // full container names
01266         foreach ( $paths as $path ) {
01267             list( $fullCont, , ) = $this->resolveStoragePath( $path );
01268             $fullConts[] = $fullCont;
01269         }
01270         // Load from the persistent file and container caches
01271         $this->primeContainerCache( $fullConts );
01272         $this->primeFileCache( $paths );
01273     }
01274 
01275     final public function clearCache( array $paths = null ) {
01276         if ( is_array( $paths ) ) {
01277             $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
01278             $paths = array_filter( $paths, 'strlen' ); // remove nulls
01279         }
01280         if ( $paths === null ) {
01281             $this->cheapCache->clear();
01282             $this->expensiveCache->clear();
01283         } else {
01284             foreach ( $paths as $path ) {
01285                 $this->cheapCache->clear( $path );
01286                 $this->expensiveCache->clear( $path );
01287             }
01288         }
01289         $this->doClearCache( $paths );
01290     }
01291 
01299     protected function doClearCache( array $paths = null ) {
01300     }
01301 
01302     final public function preloadFileStat( array $params ) {
01303         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01304         $success = true; // no network errors
01305 
01306         $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
01307         $stats = $this->doGetFileStatMulti( $params );
01308         if ( $stats === null ) {
01309             return true; // not supported
01310         }
01311 
01312         $latest = !empty( $params['latest'] ); // use latest data?
01313         foreach ( $stats as $path => $stat ) {
01314             $path = FileBackend::normalizeStoragePath( $path );
01315             if ( $path === null ) {
01316                 continue; // this shouldn't happen
01317             }
01318             if ( is_array( $stat ) ) { // file exists
01319                 // Strongly consistent backends can automatically set "latest"
01320                 $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
01321                 $this->cheapCache->set( $path, 'stat', $stat );
01322                 $this->setFileCache( $path, $stat ); // update persistent cache
01323                 if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
01324                     $this->cheapCache->set( $path, 'sha1',
01325                         array( 'hash' => $stat['sha1'], 'latest' => $latest ) );
01326                 }
01327                 if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
01328                     $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
01329                     $this->cheapCache->set( $path, 'xattr',
01330                         array( 'map' => $stat['xattr'], 'latest' => $latest ) );
01331                 }
01332             } elseif ( $stat === false ) { // file does not exist
01333                 $this->cheapCache->set( $path, 'stat',
01334                     $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
01335                 $this->cheapCache->set( $path, 'xattr',
01336                     array( 'map' => false, 'latest' => $latest ) );
01337                 $this->cheapCache->set( $path, 'sha1',
01338                     array( 'hash' => false, 'latest' => $latest ) );
01339                 wfDebug( __METHOD__ . ": File $path does not exist.\n" );
01340             } else { // an error occurred
01341                 $success = false;
01342                 wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
01343             }
01344         }
01345 
01346         return $success;
01347     }
01348 
01360     protected function doGetFileStatMulti( array $params ) {
01361         return null; // not supported
01362     }
01363 
01371     abstract protected function directoriesAreVirtual();
01372 
01380     final protected static function isValidContainerName( $container ) {
01381         // This accounts for Swift and S3 restrictions while leaving room
01382         // for things like '.xxx' (hex shard chars) or '.seg' (segments).
01383         // This disallows directory separators or traversal characters.
01384         // Note that matching strings URL encode to the same string;
01385         // in Swift, the length restriction is *after* URL encoding.
01386         return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container );
01387     }
01388 
01402     final protected function resolveStoragePath( $storagePath ) {
01403         list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
01404         if ( $backend === $this->name ) { // must be for this backend
01405             $relPath = self::normalizeContainerPath( $relPath );
01406             if ( $relPath !== null ) {
01407                 // Get shard for the normalized path if this container is sharded
01408                 $cShard = $this->getContainerShard( $container, $relPath );
01409                 // Validate and sanitize the relative path (backend-specific)
01410                 $relPath = $this->resolveContainerPath( $container, $relPath );
01411                 if ( $relPath !== null ) {
01412                     // Prepend any wiki ID prefix to the container name
01413                     $container = $this->fullContainerName( $container );
01414                     if ( self::isValidContainerName( $container ) ) {
01415                         // Validate and sanitize the container name (backend-specific)
01416                         $container = $this->resolveContainerName( "{$container}{$cShard}" );
01417                         if ( $container !== null ) {
01418                             return array( $container, $relPath, $cShard );
01419                         }
01420                     }
01421                 }
01422             }
01423         }
01424 
01425         return array( null, null, null );
01426     }
01427 
01443     final protected function resolveStoragePathReal( $storagePath ) {
01444         list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
01445         if ( $cShard !== null && substr( $relPath, -1 ) !== '/' ) {
01446             return array( $container, $relPath );
01447         }
01448 
01449         return array( null, null );
01450     }
01451 
01460     final protected function getContainerShard( $container, $relPath ) {
01461         list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
01462         if ( $levels == 1 || $levels == 2 ) {
01463             // Hash characters are either base 16 or 36
01464             $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
01465             // Get a regex that represents the shard portion of paths.
01466             // The concatenation of the captures gives us the shard.
01467             if ( $levels === 1 ) { // 16 or 36 shards per container
01468                 $hashDirRegex = '(' . $char . ')';
01469             } else { // 256 or 1296 shards per container
01470                 if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
01471                     $hashDirRegex = $char . '/(' . $char . '{2})';
01472                 } else { // short hash dir format (e.g. "a/b/c")
01473                     $hashDirRegex = '(' . $char . ')/(' . $char . ')';
01474                 }
01475             }
01476             // Allow certain directories to be above the hash dirs so as
01477             // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
01478             // They must be 2+ chars to avoid any hash directory ambiguity.
01479             $m = array();
01480             if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
01481                 return '.' . implode( '', array_slice( $m, 1 ) );
01482             }
01483 
01484             return null; // failed to match
01485         }
01486 
01487         return ''; // no sharding
01488     }
01489 
01498     final public function isSingleShardPathInternal( $storagePath ) {
01499         list( , , $shard ) = $this->resolveStoragePath( $storagePath );
01500 
01501         return ( $shard !== null );
01502     }
01503 
01512     final protected function getContainerHashLevels( $container ) {
01513         if ( isset( $this->shardViaHashLevels[$container] ) ) {
01514             $config = $this->shardViaHashLevels[$container];
01515             $hashLevels = (int)$config['levels'];
01516             if ( $hashLevels == 1 || $hashLevels == 2 ) {
01517                 $hashBase = (int)$config['base'];
01518                 if ( $hashBase == 16 || $hashBase == 36 ) {
01519                     return array( $hashLevels, $hashBase, $config['repeat'] );
01520                 }
01521             }
01522         }
01523 
01524         return array( 0, 0, false ); // no sharding
01525     }
01526 
01533     final protected function getContainerSuffixes( $container ) {
01534         $shards = array();
01535         list( $digits, $base ) = $this->getContainerHashLevels( $container );
01536         if ( $digits > 0 ) {
01537             $numShards = pow( $base, $digits );
01538             for ( $index = 0; $index < $numShards; $index++ ) {
01539                 $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits );
01540             }
01541         }
01542 
01543         return $shards;
01544     }
01545 
01552     final protected function fullContainerName( $container ) {
01553         if ( $this->wikiId != '' ) {
01554             return "{$this->wikiId}-$container";
01555         } else {
01556             return $container;
01557         }
01558     }
01559 
01568     protected function resolveContainerName( $container ) {
01569         return $container;
01570     }
01571 
01582     protected function resolveContainerPath( $container, $relStoragePath ) {
01583         return $relStoragePath;
01584     }
01585 
01592     private function containerCacheKey( $container ) {
01593         return "filebackend:{$this->name}:{$this->wikiId}:container:{$container}";
01594     }
01595 
01602     final protected function setContainerCache( $container, array $val ) {
01603         $this->memCache->add( $this->containerCacheKey( $container ), $val, 14 * 86400 );
01604     }
01605 
01612     final protected function deleteContainerCache( $container ) {
01613         if ( !$this->memCache->set( $this->containerCacheKey( $container ), 'PURGED', 300 ) ) {
01614             trigger_error( "Unable to delete stat cache for container $container." );
01615         }
01616     }
01617 
01625     final protected function primeContainerCache( array $items ) {
01626         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01627 
01628         $paths = array(); // list of storage paths
01629         $contNames = array(); // (cache key => resolved container name)
01630         // Get all the paths/containers from the items...
01631         foreach ( $items as $item ) {
01632             if ( self::isStoragePath( $item ) ) {
01633                 $paths[] = $item;
01634             } elseif ( is_string( $item ) ) { // full container name
01635                 $contNames[$this->containerCacheKey( $item )] = $item;
01636             }
01637         }
01638         // Get all the corresponding cache keys for paths...
01639         foreach ( $paths as $path ) {
01640             list( $fullCont, , ) = $this->resolveStoragePath( $path );
01641             if ( $fullCont !== null ) { // valid path for this backend
01642                 $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
01643             }
01644         }
01645 
01646         $contInfo = array(); // (resolved container name => cache value)
01647         // Get all cache entries for these container cache keys...
01648         $values = $this->memCache->getMulti( array_keys( $contNames ) );
01649         foreach ( $values as $cacheKey => $val ) {
01650             $contInfo[$contNames[$cacheKey]] = $val;
01651         }
01652 
01653         // Populate the container process cache for the backend...
01654         $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
01655     }
01656 
01664     protected function doPrimeContainerCache( array $containerInfo ) {
01665     }
01666 
01673     private function fileCacheKey( $path ) {
01674         return "filebackend:{$this->name}:{$this->wikiId}:file:" . sha1( $path );
01675     }
01676 
01685     final protected function setFileCache( $path, array $val ) {
01686         $path = FileBackend::normalizeStoragePath( $path );
01687         if ( $path === null ) {
01688             return; // invalid storage path
01689         }
01690         $age = time() - wfTimestamp( TS_UNIX, $val['mtime'] );
01691         $ttl = min( 7 * 86400, max( 300, floor( .1 * $age ) ) );
01692         $key = $this->fileCacheKey( $path );
01693         // Set the cache unless it is currently salted with the value "PURGED".
01694         // Using add() handles this except it also is a no-op in that case where
01695         // the current value is not "latest" but $val is, so use CAS in that case.
01696         if ( !$this->memCache->add( $key, $val, $ttl ) && !empty( $val['latest'] ) ) {
01697             $this->memCache->merge(
01698                 $key,
01699                 function ( BagOStuff $cache, $key, $cValue ) use ( $val ) {
01700                     return ( is_array( $cValue ) && empty( $cValue['latest'] ) )
01701                         ? $val // update the stat cache with the lastest info
01702                         : false; // do nothing (cache is salted or some error happened)
01703                 },
01704                 $ttl,
01705                 1
01706             );
01707         }
01708     }
01709 
01718     final protected function deleteFileCache( $path ) {
01719         $path = FileBackend::normalizeStoragePath( $path );
01720         if ( $path === null ) {
01721             return; // invalid storage path
01722         }
01723         if ( !$this->memCache->set( $this->fileCacheKey( $path ), 'PURGED', 300 ) ) {
01724             trigger_error( "Unable to delete stat cache for file $path." );
01725         }
01726     }
01727 
01735     final protected function primeFileCache( array $items ) {
01736         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01737 
01738         $paths = array(); // list of storage paths
01739         $pathNames = array(); // (cache key => storage path)
01740         // Get all the paths/containers from the items...
01741         foreach ( $items as $item ) {
01742             if ( self::isStoragePath( $item ) ) {
01743                 $paths[] = FileBackend::normalizeStoragePath( $item );
01744             }
01745         }
01746         // Get rid of any paths that failed normalization...
01747         $paths = array_filter( $paths, 'strlen' ); // remove nulls
01748         // Get all the corresponding cache keys for paths...
01749         foreach ( $paths as $path ) {
01750             list( , $rel, ) = $this->resolveStoragePath( $path );
01751             if ( $rel !== null ) { // valid path for this backend
01752                 $pathNames[$this->fileCacheKey( $path )] = $path;
01753             }
01754         }
01755         // Get all cache entries for these container cache keys...
01756         $values = $this->memCache->getMulti( array_keys( $pathNames ) );
01757         foreach ( $values as $cacheKey => $val ) {
01758             if ( is_array( $val ) ) {
01759                 $path = $pathNames[$cacheKey];
01760                 $this->cheapCache->set( $path, 'stat', $val );
01761                 if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
01762                     $this->cheapCache->set( $path, 'sha1',
01763                         array( 'hash' => $val['sha1'], 'latest' => $val['latest'] ) );
01764                 }
01765                 if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata
01766                     $val['xattr'] = self::normalizeXAttributes( $val['xattr'] );
01767                     $this->cheapCache->set( $path, 'xattr',
01768                         array( 'map' => $val['xattr'], 'latest' => $val['latest'] ) );
01769                 }
01770             }
01771         }
01772     }
01773 
01781     final protected static function normalizeXAttributes( array $xattr ) {
01782         $newXAttr = array( 'headers' => array(), 'metadata' => array() );
01783 
01784         foreach ( $xattr['headers'] as $name => $value ) {
01785             $newXAttr['headers'][strtolower( $name )] = $value;
01786         }
01787 
01788         foreach ( $xattr['metadata'] as $name => $value ) {
01789             $newXAttr['metadata'][strtolower( $name )] = $value;
01790         }
01791 
01792         return $newXAttr;
01793     }
01794 
01801     final protected function setConcurrencyFlags( array $opts ) {
01802         $opts['concurrency'] = 1; // off
01803         if ( $this->parallelize === 'implicit' ) {
01804             if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
01805                 $opts['concurrency'] = $this->concurrency;
01806             }
01807         } elseif ( $this->parallelize === 'explicit' ) {
01808             if ( !empty( $opts['parallelize'] ) ) {
01809                 $opts['concurrency'] = $this->concurrency;
01810             }
01811         }
01812 
01813         return $opts;
01814     }
01815 
01824     protected function getContentType( $storagePath, $content, $fsPath ) {
01825         return call_user_func_array( $this->mimeCallback, func_get_args() );
01826     }
01827 }
01828 
01837 abstract class FileBackendStoreOpHandle {
01839     public $params = array(); // params to caller functions
01841     public $backend;
01843     public $resourcesToClose = array();
01844 
01845     public $call; // string; name that identifies the function called
01846 
01850     public function closeResources() {
01851         array_map( 'fclose', $this->resourcesToClose );
01852     }
01853 }
01854 
01861 abstract class FileBackendStoreShardListIterator extends FilterIterator {
01863     protected $backend;
01864 
01866     protected $params;
01867 
01869     protected $container;
01870 
01872     protected $directory;
01873 
01875     protected $multiShardPaths = array(); // (rel path => 1)
01876 
01884     public function __construct(
01885         FileBackendStore $backend, $container, $dir, array $suffixes, array $params
01886     ) {
01887         $this->backend = $backend;
01888         $this->container = $container;
01889         $this->directory = $dir;
01890         $this->params = $params;
01891 
01892         $iter = new AppendIterator();
01893         foreach ( $suffixes as $suffix ) {
01894             $iter->append( $this->listFromShard( $this->container . $suffix ) );
01895         }
01896 
01897         parent::__construct( $iter );
01898     }
01899 
01900     public function accept() {
01901         $rel = $this->getInnerIterator()->current(); // path relative to given directory
01902         $path = $this->params['dir'] . "/{$rel}"; // full storage path
01903         if ( $this->backend->isSingleShardPathInternal( $path ) ) {
01904             return true; // path is only on one shard; no issue with duplicates
01905         } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
01906             // Don't keep listing paths that are on multiple shards
01907             return false;
01908         } else {
01909             $this->multiShardPaths[$rel] = 1;
01910 
01911             return true;
01912         }
01913     }
01914 
01915     public function rewind() {
01916         parent::rewind();
01917         $this->multiShardPaths = array();
01918     }
01919 
01926     abstract protected function listFromShard( $container );
01927 }
01928 
01932 class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
01933     protected function listFromShard( $container ) {
01934         $list = $this->backend->getDirectoryListInternal(
01935             $container, $this->directory, $this->params );
01936         if ( $list === null ) {
01937             return new ArrayIterator( array() );
01938         } else {
01939             return is_array( $list ) ? new ArrayIterator( $list ) : $list;
01940         }
01941     }
01942 }
01943 
01947 class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
01948     protected function listFromShard( $container ) {
01949         $list = $this->backend->getFileListInternal(
01950             $container, $this->directory, $this->params );
01951         if ( $list === null ) {
01952             return new ArrayIterator( array() );
01953         } else {
01954             return is_array( $list ) ? new ArrayIterator( $list ) : $list;
01955         }
01956     }
01957 }