MediaWiki  REL1_23
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 ) . " failed to concatenate " .
00361                     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         // Load from the persistent container caches
01106         $this->primeContainerCache( $paths );
01107         // Get the latest stat info for all the files (having locked them)
01108         $this->preloadFileStat( array( 'srcs' => $paths, 'latest' => true ) );
01109 
01110         // Actually attempt the operation batch...
01111         $opts = $this->setConcurrencyFlags( $opts );
01112         $subStatus = FileOpBatch::attempt( $performOps, $opts, $this->fileJournal );
01113 
01114         // Merge errors into status fields
01115         $status->merge( $subStatus );
01116         $status->success = $subStatus->success; // not done in merge()
01117 
01118         return $status;
01119     }
01120 
01121     final protected function doQuickOperationsInternal( array $ops ) {
01122         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01123         $status = Status::newGood();
01124 
01125         // Fix up custom header name/value pairs...
01126         $ops = array_map( array( $this, 'stripInvalidHeadersFromOp' ), $ops );
01127 
01128         // Clear any file cache entries
01129         $this->clearCache();
01130 
01131         $supportedOps = array( 'create', 'store', 'copy', 'move', 'delete', 'describe', 'null' );
01132         // Parallel ops may be disabled in config due to dependencies (e.g. needing popen())
01133         $async = ( $this->parallelize === 'implicit' && count( $ops ) > 1 );
01134         $maxConcurrency = $this->concurrency; // throttle
01135 
01136         $statuses = array(); // array of (index => Status)
01137         $fileOpHandles = array(); // list of (index => handle) arrays
01138         $curFileOpHandles = array(); // current handle batch
01139         // Perform the sync-only ops and build up op handles for the async ops...
01140         foreach ( $ops as $index => $params ) {
01141             if ( !in_array( $params['op'], $supportedOps ) ) {
01142                 throw new FileBackendError( "Operation '{$params['op']}' is not supported." );
01143             }
01144             $method = $params['op'] . 'Internal'; // e.g. "storeInternal"
01145             $subStatus = $this->$method( array( 'async' => $async ) + $params );
01146             if ( $subStatus->value instanceof FileBackendStoreOpHandle ) { // async
01147                 if ( count( $curFileOpHandles ) >= $maxConcurrency ) {
01148                     $fileOpHandles[] = $curFileOpHandles; // push this batch
01149                     $curFileOpHandles = array();
01150                 }
01151                 $curFileOpHandles[$index] = $subStatus->value; // keep index
01152             } else { // error or completed
01153                 $statuses[$index] = $subStatus; // keep index
01154             }
01155         }
01156         if ( count( $curFileOpHandles ) ) {
01157             $fileOpHandles[] = $curFileOpHandles; // last batch
01158         }
01159         // Do all the async ops that can be done concurrently...
01160         foreach ( $fileOpHandles as $fileHandleBatch ) {
01161             $statuses = $statuses + $this->executeOpHandlesInternal( $fileHandleBatch );
01162         }
01163         // Marshall and merge all the responses...
01164         foreach ( $statuses as $index => $subStatus ) {
01165             $status->merge( $subStatus );
01166             if ( $subStatus->isOK() ) {
01167                 $status->success[$index] = true;
01168                 ++$status->successCount;
01169             } else {
01170                 $status->success[$index] = false;
01171                 ++$status->failCount;
01172             }
01173         }
01174 
01175         return $status;
01176     }
01177 
01188     final public function executeOpHandlesInternal( array $fileOpHandles ) {
01189         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01190 
01191         foreach ( $fileOpHandles as $fileOpHandle ) {
01192             if ( !( $fileOpHandle instanceof FileBackendStoreOpHandle ) ) {
01193                 throw new FileBackendError( "Given a non-FileBackendStoreOpHandle object." );
01194             } elseif ( $fileOpHandle->backend->getName() !== $this->getName() ) {
01195                 throw new FileBackendError( "Given a FileBackendStoreOpHandle for the wrong backend." );
01196             }
01197         }
01198         $res = $this->doExecuteOpHandlesInternal( $fileOpHandles );
01199         foreach ( $fileOpHandles as $fileOpHandle ) {
01200             $fileOpHandle->closeResources();
01201         }
01202 
01203         return $res;
01204     }
01205 
01212     protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
01213         if ( count( $fileOpHandles ) ) {
01214             throw new FileBackendError( "This backend supports no asynchronous operations." );
01215         }
01216 
01217         return array();
01218     }
01219 
01229     protected function stripInvalidHeadersFromOp( array $op ) {
01230         static $longs = array( 'Content-Disposition' );
01231         if ( isset( $op['headers'] ) ) { // op sets HTTP headers
01232             foreach ( $op['headers'] as $name => $value ) {
01233                 $maxHVLen = in_array( $name, $longs ) ? INF : 255;
01234                 if ( strlen( $name ) > 255 || strlen( $value ) > $maxHVLen ) {
01235                     trigger_error( "Header '$name: $value' is too long." );
01236                     unset( $op['headers'][$name] );
01237                 } elseif ( !strlen( $value ) ) {
01238                     $op['headers'][$name] = ''; // null/false => ""
01239                 }
01240             }
01241         }
01242 
01243         return $op;
01244     }
01245 
01246     final public function preloadCache( array $paths ) {
01247         $fullConts = array(); // full container names
01248         foreach ( $paths as $path ) {
01249             list( $fullCont, , ) = $this->resolveStoragePath( $path );
01250             $fullConts[] = $fullCont;
01251         }
01252         // Load from the persistent file and container caches
01253         $this->primeContainerCache( $fullConts );
01254         $this->primeFileCache( $paths );
01255     }
01256 
01257     final public function clearCache( array $paths = null ) {
01258         if ( is_array( $paths ) ) {
01259             $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
01260             $paths = array_filter( $paths, 'strlen' ); // remove nulls
01261         }
01262         if ( $paths === null ) {
01263             $this->cheapCache->clear();
01264             $this->expensiveCache->clear();
01265         } else {
01266             foreach ( $paths as $path ) {
01267                 $this->cheapCache->clear( $path );
01268                 $this->expensiveCache->clear( $path );
01269             }
01270         }
01271         $this->doClearCache( $paths );
01272     }
01273 
01281     protected function doClearCache( array $paths = null ) {
01282     }
01283 
01284     final public function preloadFileStat( array $params ) {
01285         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01286 
01287         $params['concurrency'] = ( $this->parallelize !== 'off' ) ? $this->concurrency : 1;
01288         $stats = $this->doGetFileStatMulti( $params );
01289         if ( $stats === null ) {
01290             return; // not supported
01291         }
01292 
01293         $latest = !empty( $params['latest'] ); // use latest data?
01294         foreach ( $stats as $path => $stat ) {
01295             $path = FileBackend::normalizeStoragePath( $path );
01296             if ( $path === null ) {
01297                 continue; // this shouldn't happen
01298             }
01299             if ( is_array( $stat ) ) { // file exists
01300                 // Strongly consistent backends can automatically set "latest"
01301                 $stat['latest'] = isset( $stat['latest'] ) ? $stat['latest'] : $latest;
01302                 $this->cheapCache->set( $path, 'stat', $stat );
01303                 $this->setFileCache( $path, $stat ); // update persistent cache
01304                 if ( isset( $stat['sha1'] ) ) { // some backends store SHA-1 as metadata
01305                     $this->cheapCache->set( $path, 'sha1',
01306                         array( 'hash' => $stat['sha1'], 'latest' => $latest ) );
01307                 }
01308                 if ( isset( $stat['xattr'] ) ) { // some backends store headers/metadata
01309                     $stat['xattr'] = self::normalizeXAttributes( $stat['xattr'] );
01310                     $this->cheapCache->set( $path, 'xattr',
01311                         array( 'map' => $stat['xattr'], 'latest' => $latest ) );
01312                 }
01313             } elseif ( $stat === false ) { // file does not exist
01314                 $this->cheapCache->set( $path, 'stat',
01315                     $latest ? 'NOT_EXIST_LATEST' : 'NOT_EXIST' );
01316                 $this->cheapCache->set( $path, 'xattr',
01317                     array( 'map' => false, 'latest' => $latest ) );
01318                 $this->cheapCache->set( $path, 'sha1',
01319                     array( 'hash' => false, 'latest' => $latest ) );
01320                 wfDebug( __METHOD__ . ": File $path does not exist.\n" );
01321             } else { // an error occurred
01322                 wfDebug( __METHOD__ . ": Could not stat file $path.\n" );
01323             }
01324         }
01325     }
01326 
01338     protected function doGetFileStatMulti( array $params ) {
01339         return null; // not supported
01340     }
01341 
01349     abstract protected function directoriesAreVirtual();
01350 
01358     final protected static function isValidContainerName( $container ) {
01359         // This accounts for Swift and S3 restrictions while leaving room
01360         // for things like '.xxx' (hex shard chars) or '.seg' (segments).
01361         // This disallows directory separators or traversal characters.
01362         // Note that matching strings URL encode to the same string;
01363         // in Swift, the length restriction is *after* URL encoding.
01364         return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container );
01365     }
01366 
01380     final protected function resolveStoragePath( $storagePath ) {
01381         list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
01382         if ( $backend === $this->name ) { // must be for this backend
01383             $relPath = self::normalizeContainerPath( $relPath );
01384             if ( $relPath !== null ) {
01385                 // Get shard for the normalized path if this container is sharded
01386                 $cShard = $this->getContainerShard( $container, $relPath );
01387                 // Validate and sanitize the relative path (backend-specific)
01388                 $relPath = $this->resolveContainerPath( $container, $relPath );
01389                 if ( $relPath !== null ) {
01390                     // Prepend any wiki ID prefix to the container name
01391                     $container = $this->fullContainerName( $container );
01392                     if ( self::isValidContainerName( $container ) ) {
01393                         // Validate and sanitize the container name (backend-specific)
01394                         $container = $this->resolveContainerName( "{$container}{$cShard}" );
01395                         if ( $container !== null ) {
01396                             return array( $container, $relPath, $cShard );
01397                         }
01398                     }
01399                 }
01400             }
01401         }
01402 
01403         return array( null, null, null );
01404     }
01405 
01421     final protected function resolveStoragePathReal( $storagePath ) {
01422         list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
01423         if ( $cShard !== null && substr( $relPath, -1 ) !== '/' ) {
01424             return array( $container, $relPath );
01425         }
01426 
01427         return array( null, null );
01428     }
01429 
01438     final protected function getContainerShard( $container, $relPath ) {
01439         list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
01440         if ( $levels == 1 || $levels == 2 ) {
01441             // Hash characters are either base 16 or 36
01442             $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
01443             // Get a regex that represents the shard portion of paths.
01444             // The concatenation of the captures gives us the shard.
01445             if ( $levels === 1 ) { // 16 or 36 shards per container
01446                 $hashDirRegex = '(' . $char . ')';
01447             } else { // 256 or 1296 shards per container
01448                 if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
01449                     $hashDirRegex = $char . '/(' . $char . '{2})';
01450                 } else { // short hash dir format (e.g. "a/b/c")
01451                     $hashDirRegex = '(' . $char . ')/(' . $char . ')';
01452                 }
01453             }
01454             // Allow certain directories to be above the hash dirs so as
01455             // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
01456             // They must be 2+ chars to avoid any hash directory ambiguity.
01457             $m = array();
01458             if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
01459                 return '.' . implode( '', array_slice( $m, 1 ) );
01460             }
01461 
01462             return null; // failed to match
01463         }
01464 
01465         return ''; // no sharding
01466     }
01467 
01476     final public function isSingleShardPathInternal( $storagePath ) {
01477         list( , , $shard ) = $this->resolveStoragePath( $storagePath );
01478 
01479         return ( $shard !== null );
01480     }
01481 
01490     final protected function getContainerHashLevels( $container ) {
01491         if ( isset( $this->shardViaHashLevels[$container] ) ) {
01492             $config = $this->shardViaHashLevels[$container];
01493             $hashLevels = (int)$config['levels'];
01494             if ( $hashLevels == 1 || $hashLevels == 2 ) {
01495                 $hashBase = (int)$config['base'];
01496                 if ( $hashBase == 16 || $hashBase == 36 ) {
01497                     return array( $hashLevels, $hashBase, $config['repeat'] );
01498                 }
01499             }
01500         }
01501 
01502         return array( 0, 0, false ); // no sharding
01503     }
01504 
01511     final protected function getContainerSuffixes( $container ) {
01512         $shards = array();
01513         list( $digits, $base ) = $this->getContainerHashLevels( $container );
01514         if ( $digits > 0 ) {
01515             $numShards = pow( $base, $digits );
01516             for ( $index = 0; $index < $numShards; $index++ ) {
01517                 $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits );
01518             }
01519         }
01520 
01521         return $shards;
01522     }
01523 
01530     final protected function fullContainerName( $container ) {
01531         if ( $this->wikiId != '' ) {
01532             return "{$this->wikiId}-$container";
01533         } else {
01534             return $container;
01535         }
01536     }
01537 
01546     protected function resolveContainerName( $container ) {
01547         return $container;
01548     }
01549 
01560     protected function resolveContainerPath( $container, $relStoragePath ) {
01561         return $relStoragePath;
01562     }
01563 
01570     private function containerCacheKey( $container ) {
01571         return "filebackend:{$this->name}:{$this->wikiId}:container:{$container}";
01572     }
01573 
01580     final protected function setContainerCache( $container, array $val ) {
01581         $this->memCache->add( $this->containerCacheKey( $container ), $val, 14 * 86400 );
01582     }
01583 
01590     final protected function deleteContainerCache( $container ) {
01591         if ( !$this->memCache->set( $this->containerCacheKey( $container ), 'PURGED', 300 ) ) {
01592             trigger_error( "Unable to delete stat cache for container $container." );
01593         }
01594     }
01595 
01603     final protected function primeContainerCache( array $items ) {
01604         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01605 
01606         $paths = array(); // list of storage paths
01607         $contNames = array(); // (cache key => resolved container name)
01608         // Get all the paths/containers from the items...
01609         foreach ( $items as $item ) {
01610             if ( self::isStoragePath( $item ) ) {
01611                 $paths[] = $item;
01612             } elseif ( is_string( $item ) ) { // full container name
01613                 $contNames[$this->containerCacheKey( $item )] = $item;
01614             }
01615         }
01616         // Get all the corresponding cache keys for paths...
01617         foreach ( $paths as $path ) {
01618             list( $fullCont, , ) = $this->resolveStoragePath( $path );
01619             if ( $fullCont !== null ) { // valid path for this backend
01620                 $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
01621             }
01622         }
01623 
01624         $contInfo = array(); // (resolved container name => cache value)
01625         // Get all cache entries for these container cache keys...
01626         $values = $this->memCache->getMulti( array_keys( $contNames ) );
01627         foreach ( $values as $cacheKey => $val ) {
01628             $contInfo[$contNames[$cacheKey]] = $val;
01629         }
01630 
01631         // Populate the container process cache for the backend...
01632         $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
01633     }
01634 
01642     protected function doPrimeContainerCache( array $containerInfo ) {
01643     }
01644 
01651     private function fileCacheKey( $path ) {
01652         return "filebackend:{$this->name}:{$this->wikiId}:file:" . sha1( $path );
01653     }
01654 
01663     final protected function setFileCache( $path, array $val ) {
01664         $path = FileBackend::normalizeStoragePath( $path );
01665         if ( $path === null ) {
01666             return; // invalid storage path
01667         }
01668         $age = time() - wfTimestamp( TS_UNIX, $val['mtime'] );
01669         $ttl = min( 7 * 86400, max( 300, floor( .1 * $age ) ) );
01670         $key = $this->fileCacheKey( $path );
01671         // Set the cache unless it is currently salted with the value "PURGED".
01672         // Using add() handles this except it also is a no-op in that case where
01673         // the current value is not "latest" but $val is, so use CAS in that case.
01674         if ( !$this->memCache->add( $key, $val, $ttl ) && !empty( $val['latest'] ) ) {
01675             $this->memCache->merge(
01676                 $key,
01677                 function( BagOStuff $cache, $key, $cValue ) use ( $val ) {
01678                     return ( is_array( $cValue ) && empty( $cValue['latest'] ) )
01679                         ? $val // update the stat cache with the lastest info
01680                         : false; // do nothing (cache is salted or some error happened)
01681                 },
01682                 $ttl,
01683                 1
01684             );
01685         }
01686     }
01687 
01696     final protected function deleteFileCache( $path ) {
01697         $path = FileBackend::normalizeStoragePath( $path );
01698         if ( $path === null ) {
01699             return; // invalid storage path
01700         }
01701         if ( !$this->memCache->set( $this->fileCacheKey( $path ), 'PURGED', 300 ) ) {
01702             trigger_error( "Unable to delete stat cache for file $path." );
01703         }
01704     }
01705 
01713     final protected function primeFileCache( array $items ) {
01714         $section = new ProfileSection( __METHOD__ . "-{$this->name}" );
01715 
01716         $paths = array(); // list of storage paths
01717         $pathNames = array(); // (cache key => storage path)
01718         // Get all the paths/containers from the items...
01719         foreach ( $items as $item ) {
01720             if ( self::isStoragePath( $item ) ) {
01721                 $paths[] = FileBackend::normalizeStoragePath( $item );
01722             }
01723         }
01724         // Get rid of any paths that failed normalization...
01725         $paths = array_filter( $paths, 'strlen' ); // remove nulls
01726         // Get all the corresponding cache keys for paths...
01727         foreach ( $paths as $path ) {
01728             list( , $rel, ) = $this->resolveStoragePath( $path );
01729             if ( $rel !== null ) { // valid path for this backend
01730                 $pathNames[$this->fileCacheKey( $path )] = $path;
01731             }
01732         }
01733         // Get all cache entries for these container cache keys...
01734         $values = $this->memCache->getMulti( array_keys( $pathNames ) );
01735         foreach ( $values as $cacheKey => $val ) {
01736             if ( is_array( $val ) ) {
01737                 $path = $pathNames[$cacheKey];
01738                 $this->cheapCache->set( $path, 'stat', $val );
01739                 if ( isset( $val['sha1'] ) ) { // some backends store SHA-1 as metadata
01740                     $this->cheapCache->set( $path, 'sha1',
01741                         array( 'hash' => $val['sha1'], 'latest' => $val['latest'] ) );
01742                 }
01743                 if ( isset( $val['xattr'] ) ) { // some backends store headers/metadata
01744                     $val['xattr'] = self::normalizeXAttributes( $val['xattr'] );
01745                     $this->cheapCache->set( $path, 'xattr',
01746                         array( 'map' => $val['xattr'], 'latest' => $val['latest'] ) );
01747                 }
01748             }
01749         }
01750     }
01751 
01759     final protected static function normalizeXAttributes( array $xattr ) {
01760         $newXAttr = array( 'headers' => array(), 'metadata' => array() );
01761 
01762         foreach ( $xattr['headers'] as $name => $value ) {
01763             $newXAttr['headers'][strtolower( $name )] = $value;
01764         }
01765 
01766         foreach ( $xattr['metadata'] as $name => $value ) {
01767             $newXAttr['metadata'][strtolower( $name )] = $value;
01768         }
01769 
01770         return $newXAttr;
01771     }
01772 
01779     final protected function setConcurrencyFlags( array $opts ) {
01780         $opts['concurrency'] = 1; // off
01781         if ( $this->parallelize === 'implicit' ) {
01782             if ( !isset( $opts['parallelize'] ) || $opts['parallelize'] ) {
01783                 $opts['concurrency'] = $this->concurrency;
01784             }
01785         } elseif ( $this->parallelize === 'explicit' ) {
01786             if ( !empty( $opts['parallelize'] ) ) {
01787                 $opts['concurrency'] = $this->concurrency;
01788             }
01789         }
01790 
01791         return $opts;
01792     }
01793 
01802     protected function getContentType( $storagePath, $content, $fsPath ) {
01803         return call_user_func_array( $this->mimeCallback, func_get_args() );
01804     }
01805 }
01806 
01815 abstract class FileBackendStoreOpHandle {
01817     public $params = array(); // params to caller functions
01819     public $backend;
01821     public $resourcesToClose = array();
01822 
01823     public $call; // string; name that identifies the function called
01824 
01828     public function closeResources() {
01829         array_map( 'fclose', $this->resourcesToClose );
01830     }
01831 }
01832 
01839 abstract class FileBackendStoreShardListIterator extends FilterIterator {
01841     protected $backend;
01842 
01844     protected $params;
01845 
01847     protected $container;
01848 
01850     protected $directory;
01851 
01853     protected $multiShardPaths = array(); // (rel path => 1)
01854 
01862     public function __construct(
01863         FileBackendStore $backend, $container, $dir, array $suffixes, array $params
01864     ) {
01865         $this->backend = $backend;
01866         $this->container = $container;
01867         $this->directory = $dir;
01868         $this->params = $params;
01869 
01870         $iter = new AppendIterator();
01871         foreach ( $suffixes as $suffix ) {
01872             $iter->append( $this->listFromShard( $this->container . $suffix ) );
01873         }
01874 
01875         parent::__construct( $iter );
01876     }
01877 
01878     public function accept() {
01879         $rel = $this->getInnerIterator()->current(); // path relative to given directory
01880         $path = $this->params['dir'] . "/{$rel}"; // full storage path
01881         if ( $this->backend->isSingleShardPathInternal( $path ) ) {
01882             return true; // path is only on one shard; no issue with duplicates
01883         } elseif ( isset( $this->multiShardPaths[$rel] ) ) {
01884             // Don't keep listing paths that are on multiple shards
01885             return false;
01886         } else {
01887             $this->multiShardPaths[$rel] = 1;
01888 
01889             return true;
01890         }
01891     }
01892 
01893     public function rewind() {
01894         parent::rewind();
01895         $this->multiShardPaths = array();
01896     }
01897 
01904     abstract protected function listFromShard( $container );
01905 }
01906 
01910 class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
01911     protected function listFromShard( $container ) {
01912         $list = $this->backend->getDirectoryListInternal(
01913             $container, $this->directory, $this->params );
01914         if ( $list === null ) {
01915             return new ArrayIterator( array() );
01916         } else {
01917             return is_array( $list ) ? new ArrayIterator( $list ) : $list;
01918         }
01919     }
01920 }
01921 
01925 class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
01926     protected function listFromShard( $container ) {
01927         $list = $this->backend->getFileListInternal(
01928             $container, $this->directory, $this->params );
01929         if ( $list === null ) {
01930             return new ArrayIterator( array() );
01931         } else {
01932             return is_array( $list ) ? new ArrayIterator( $list ) : $list;
01933         }
01934     }
01935 }