MediaWiki
REL1_19
|
00001 <?php 00040 abstract class FileBackend { 00041 protected $name; // string; unique backend name 00042 protected $wikiId; // string; unique wiki name 00043 protected $readOnly; // string; read-only explanation message 00045 protected $lockManager; 00046 00063 public function __construct( array $config ) { 00064 $this->name = $config['name']; 00065 if ( !preg_match( '!^[a-zA-Z0-9-_]{1,255}$!', $this->name ) ) { 00066 throw new MWException( "Backend name `{$this->name}` is invalid." ); 00067 } 00068 $this->wikiId = isset( $config['wikiId'] ) 00069 ? $config['wikiId'] 00070 : wfWikiID(); // e.g. "my_wiki-en_" 00071 $this->lockManager = ( $config['lockManager'] instanceof LockManager ) 00072 ? $config['lockManager'] 00073 : LockManagerGroup::singleton()->get( $config['lockManager'] ); 00074 $this->readOnly = isset( $config['readOnly'] ) 00075 ? (string)$config['readOnly'] 00076 : ''; 00077 } 00078 00086 final public function getName() { 00087 return $this->name; 00088 } 00089 00095 final public function isReadOnly() { 00096 return ( $this->readOnly != '' ); 00097 } 00098 00104 final public function getReadOnlyReason() { 00105 return ( $this->readOnly != '' ) ? $this->readOnly : false; 00106 } 00107 00195 final public function doOperations( array $ops, array $opts = array() ) { 00196 if ( $this->isReadOnly() ) { 00197 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); 00198 } 00199 if ( empty( $opts['force'] ) ) { // sanity 00200 unset( $opts['nonLocking'] ); 00201 unset( $opts['allowStale'] ); 00202 } 00203 return $this->doOperationsInternal( $ops, $opts ); 00204 } 00205 00209 abstract protected function doOperationsInternal( array $ops, array $opts ); 00210 00222 final public function doOperation( array $op, array $opts = array() ) { 00223 return $this->doOperations( array( $op ), $opts ); 00224 } 00225 00236 final public function create( array $params, array $opts = array() ) { 00237 $params['op'] = 'create'; 00238 return $this->doOperation( $params, $opts ); 00239 } 00240 00251 final public function store( array $params, array $opts = array() ) { 00252 $params['op'] = 'store'; 00253 return $this->doOperation( $params, $opts ); 00254 } 00255 00266 final public function copy( array $params, array $opts = array() ) { 00267 $params['op'] = 'copy'; 00268 return $this->doOperation( $params, $opts ); 00269 } 00270 00281 final public function move( array $params, array $opts = array() ) { 00282 $params['op'] = 'move'; 00283 return $this->doOperation( $params, $opts ); 00284 } 00285 00296 final public function delete( array $params, array $opts = array() ) { 00297 $params['op'] = 'delete'; 00298 return $this->doOperation( $params, $opts ); 00299 } 00300 00313 abstract public function concatenate( array $params ); 00314 00326 final public function prepare( array $params ) { 00327 if ( $this->isReadOnly() ) { 00328 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); 00329 } 00330 return $this->doPrepare( $params ); 00331 } 00332 00336 abstract protected function doPrepare( array $params ); 00337 00353 final public function secure( array $params ) { 00354 if ( $this->isReadOnly() ) { 00355 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); 00356 } 00357 $status = $this->doPrepare( $params ); // dir must exist to restrict it 00358 if ( $status->isOK() ) { 00359 $status->merge( $this->doSecure( $params ) ); 00360 } 00361 return $status; 00362 } 00363 00367 abstract protected function doSecure( array $params ); 00368 00380 final public function clean( array $params ) { 00381 if ( $this->isReadOnly() ) { 00382 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly ); 00383 } 00384 return $this->doClean( $params ); 00385 } 00386 00390 abstract protected function doClean( array $params ); 00391 00403 abstract public function fileExists( array $params ); 00404 00415 abstract public function getFileTimestamp( array $params ); 00416 00428 abstract public function getFileContents( array $params ); 00429 00440 abstract public function getFileSize( array $params ); 00441 00457 abstract public function getFileStat( array $params ); 00458 00469 abstract public function getFileSha1Base36( array $params ); 00470 00482 abstract public function getFileProps( array $params ); 00483 00499 abstract public function streamFile( array $params ); 00500 00521 abstract public function getLocalReference( array $params ); 00522 00535 abstract public function getLocalCopy( array $params ); 00536 00552 abstract public function getFileList( array $params ); 00553 00561 public function clearCache( array $paths = null ) {} 00562 00573 final public function lockFiles( array $paths, $type ) { 00574 return $this->lockManager->lock( $paths, $type ); 00575 } 00576 00584 final public function unlockFiles( array $paths, $type ) { 00585 return $this->lockManager->unlock( $paths, $type ); 00586 } 00587 00601 final public function getScopedFileLocks( array $paths, $type, Status $status ) { 00602 return ScopedLock::factory( $this->lockManager, $paths, $type, $status ); 00603 } 00604 00612 final public static function isStoragePath( $path ) { 00613 return ( strpos( $path, 'mwstore://' ) === 0 ); 00614 } 00615 00624 final public static function splitStoragePath( $storagePath ) { 00625 if ( self::isStoragePath( $storagePath ) ) { 00626 // Remove the "mwstore://" prefix and split the path 00627 $parts = explode( '/', substr( $storagePath, 10 ), 3 ); 00628 if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) { 00629 if ( count( $parts ) == 3 ) { 00630 return $parts; // e.g. "backend/container/path" 00631 } else { 00632 return array( $parts[0], $parts[1], '' ); // e.g. "backend/container" 00633 } 00634 } 00635 } 00636 return array( null, null, null ); 00637 } 00638 00646 final public static function normalizeStoragePath( $storagePath ) { 00647 list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath ); 00648 if ( $relPath !== null ) { // must be for this backend 00649 $relPath = self::normalizeContainerPath( $relPath ); 00650 if ( $relPath !== null ) { 00651 return ( $relPath != '' ) 00652 ? "mwstore://{$backend}/{$container}/{$relPath}" 00653 : "mwstore://{$backend}/{$container}"; 00654 } 00655 } 00656 return null; 00657 } 00658 00667 final protected static function normalizeContainerPath( $path ) { 00668 // Normalize directory separators 00669 $path = strtr( $path, '\\', '/' ); 00670 // Collapse any consecutive directory separators 00671 $path = preg_replace( '![/]{2,}!', '/', $path ); 00672 // Remove any leading directory separator 00673 $path = ltrim( $path, '/' ); 00674 // Use the same traversal protection as Title::secureAndSplit() 00675 if ( strpos( $path, '.' ) !== false ) { 00676 if ( 00677 $path === '.' || 00678 $path === '..' || 00679 strpos( $path, './' ) === 0 || 00680 strpos( $path, '../' ) === 0 || 00681 strpos( $path, '/./' ) !== false || 00682 strpos( $path, '/../' ) !== false 00683 ) { 00684 return null; 00685 } 00686 } 00687 return $path; 00688 } 00689 00698 final public static function parentStoragePath( $storagePath ) { 00699 $storagePath = dirname( $storagePath ); 00700 list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath ); 00701 return ( $rel === null ) ? null : $storagePath; 00702 } 00703 00710 final public static function extensionFromPath( $path ) { 00711 $i = strrpos( $path, '.' ); 00712 return strtolower( $i ? substr( $path, $i + 1 ) : '' ); 00713 } 00714 } 00715 00729 abstract class FileBackendStore extends FileBackend { 00731 protected $cache = array(); // (storage path => key => value) 00732 protected $maxCacheSize = 100; // integer; max paths with entries 00734 protected $expensiveCache = array(); // (storage path => key => value) 00735 protected $maxExpensiveCacheSize = 10; // integer; max paths with entries 00736 00738 protected $shardViaHashLevels = array(); // (container name => config array) 00739 00740 protected $maxFileSize = 1000000000; // integer bytes (1GB) 00741 00749 final public function maxFileSizeInternal() { 00750 return $this->maxFileSize; 00751 } 00752 00761 abstract public function isPathUsableInternal( $storagePath ); 00762 00775 final public function createInternal( array $params ) { 00776 wfProfileIn( __METHOD__ ); 00777 if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) { 00778 $status = Status::newFatal( 'backend-fail-create', $params['dst'] ); 00779 } else { 00780 $status = $this->doCreateInternal( $params ); 00781 $this->clearCache( array( $params['dst'] ) ); 00782 } 00783 wfProfileOut( __METHOD__ ); 00784 return $status; 00785 } 00786 00790 abstract protected function doCreateInternal( array $params ); 00791 00804 final public function storeInternal( array $params ) { 00805 wfProfileIn( __METHOD__ ); 00806 if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) { 00807 $status = Status::newFatal( 'backend-fail-store', $params['dst'] ); 00808 } else { 00809 $status = $this->doStoreInternal( $params ); 00810 $this->clearCache( array( $params['dst'] ) ); 00811 } 00812 wfProfileOut( __METHOD__ ); 00813 return $status; 00814 } 00815 00819 abstract protected function doStoreInternal( array $params ); 00820 00833 final public function copyInternal( array $params ) { 00834 wfProfileIn( __METHOD__ ); 00835 $status = $this->doCopyInternal( $params ); 00836 $this->clearCache( array( $params['dst'] ) ); 00837 wfProfileOut( __METHOD__ ); 00838 return $status; 00839 } 00840 00844 abstract protected function doCopyInternal( array $params ); 00845 00857 final public function deleteInternal( array $params ) { 00858 wfProfileIn( __METHOD__ ); 00859 $status = $this->doDeleteInternal( $params ); 00860 $this->clearCache( array( $params['src'] ) ); 00861 wfProfileOut( __METHOD__ ); 00862 return $status; 00863 } 00864 00868 abstract protected function doDeleteInternal( array $params ); 00869 00882 final public function moveInternal( array $params ) { 00883 wfProfileIn( __METHOD__ ); 00884 $status = $this->doMoveInternal( $params ); 00885 $this->clearCache( array( $params['src'], $params['dst'] ) ); 00886 wfProfileOut( __METHOD__ ); 00887 return $status; 00888 } 00889 00893 protected function doMoveInternal( array $params ) { 00894 // Copy source to dest 00895 $status = $this->copyInternal( $params ); 00896 if ( $status->isOK() ) { 00897 // Delete source (only fails due to races or medium going down) 00898 $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) ); 00899 $status->setResult( true, $status->value ); // ignore delete() errors 00900 } 00901 return $status; 00902 } 00903 00907 final public function concatenate( array $params ) { 00908 wfProfileIn( __METHOD__ ); 00909 $status = Status::newGood(); 00910 00911 // Try to lock the source files for the scope of this function 00912 $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status ); 00913 if ( $status->isOK() ) { 00914 // Actually do the concatenation 00915 $status->merge( $this->doConcatenate( $params ) ); 00916 } 00917 00918 wfProfileOut( __METHOD__ ); 00919 return $status; 00920 } 00921 00925 protected function doConcatenate( array $params ) { 00926 $status = Status::newGood(); 00927 $tmpPath = $params['dst']; // convenience 00928 00929 // Check that the specified temp file is valid... 00930 wfSuppressWarnings(); 00931 $ok = ( is_file( $tmpPath ) && !filesize( $tmpPath ) ); 00932 wfRestoreWarnings(); 00933 if ( !$ok ) { // not present or not empty 00934 $status->fatal( 'backend-fail-opentemp', $tmpPath ); 00935 return $status; 00936 } 00937 00938 // Build up the temp file using the source chunks (in order)... 00939 $tmpHandle = fopen( $tmpPath, 'ab' ); 00940 if ( $tmpHandle === false ) { 00941 $status->fatal( 'backend-fail-opentemp', $tmpPath ); 00942 return $status; 00943 } 00944 foreach ( $params['srcs'] as $virtualSource ) { 00945 // Get a local FS version of the chunk 00946 $tmpFile = $this->getLocalReference( array( 'src' => $virtualSource ) ); 00947 if ( !$tmpFile ) { 00948 $status->fatal( 'backend-fail-read', $virtualSource ); 00949 return $status; 00950 } 00951 // Get a handle to the local FS version 00952 $sourceHandle = fopen( $tmpFile->getPath(), 'r' ); 00953 if ( $sourceHandle === false ) { 00954 fclose( $tmpHandle ); 00955 $status->fatal( 'backend-fail-read', $virtualSource ); 00956 return $status; 00957 } 00958 // Append chunk to file (pass chunk size to avoid magic quotes) 00959 if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) { 00960 fclose( $sourceHandle ); 00961 fclose( $tmpHandle ); 00962 $status->fatal( 'backend-fail-writetemp', $tmpPath ); 00963 return $status; 00964 } 00965 fclose( $sourceHandle ); 00966 } 00967 if ( !fclose( $tmpHandle ) ) { 00968 $status->fatal( 'backend-fail-closetemp', $tmpPath ); 00969 return $status; 00970 } 00971 00972 clearstatcache(); // temp file changed 00973 00974 return $status; 00975 } 00976 00980 final protected function doPrepare( array $params ) { 00981 wfProfileIn( __METHOD__ ); 00982 00983 $status = Status::newGood(); 00984 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); 00985 if ( $dir === null ) { 00986 $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); 00987 wfProfileOut( __METHOD__ ); 00988 return $status; // invalid storage path 00989 } 00990 00991 if ( $shard !== null ) { // confined to a single container/shard 00992 $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) ); 00993 } else { // directory is on several shards 00994 wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); 00995 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); 00996 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { 00997 $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) ); 00998 } 00999 } 01000 01001 wfProfileOut( __METHOD__ ); 01002 return $status; 01003 } 01004 01008 protected function doPrepareInternal( $container, $dir, array $params ) { 01009 return Status::newGood(); 01010 } 01011 01015 final protected function doSecure( array $params ) { 01016 wfProfileIn( __METHOD__ ); 01017 $status = Status::newGood(); 01018 01019 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); 01020 if ( $dir === null ) { 01021 $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); 01022 wfProfileOut( __METHOD__ ); 01023 return $status; // invalid storage path 01024 } 01025 01026 if ( $shard !== null ) { // confined to a single container/shard 01027 $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) ); 01028 } else { // directory is on several shards 01029 wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); 01030 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); 01031 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { 01032 $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) ); 01033 } 01034 } 01035 01036 wfProfileOut( __METHOD__ ); 01037 return $status; 01038 } 01039 01043 protected function doSecureInternal( $container, $dir, array $params ) { 01044 return Status::newGood(); 01045 } 01046 01050 final protected function doClean( array $params ) { 01051 wfProfileIn( __METHOD__ ); 01052 $status = Status::newGood(); 01053 01054 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); 01055 if ( $dir === null ) { 01056 $status->fatal( 'backend-fail-invalidpath', $params['dir'] ); 01057 wfProfileOut( __METHOD__ ); 01058 return $status; // invalid storage path 01059 } 01060 01061 // Attempt to lock this directory... 01062 $filesLockEx = array( $params['dir'] ); 01063 $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status ); 01064 if ( !$status->isOK() ) { 01065 wfProfileOut( __METHOD__ ); 01066 return $status; // abort 01067 } 01068 01069 if ( $shard !== null ) { // confined to a single container/shard 01070 $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) ); 01071 } else { // directory is on several shards 01072 wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); 01073 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); 01074 foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) { 01075 $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) ); 01076 } 01077 } 01078 01079 wfProfileOut( __METHOD__ ); 01080 return $status; 01081 } 01082 01086 protected function doCleanInternal( $container, $dir, array $params ) { 01087 return Status::newGood(); 01088 } 01089 01093 final public function fileExists( array $params ) { 01094 wfProfileIn( __METHOD__ ); 01095 $stat = $this->getFileStat( $params ); 01096 wfProfileOut( __METHOD__ ); 01097 return ( $stat === null ) ? null : (bool)$stat; // null => failure 01098 } 01099 01103 final public function getFileTimestamp( array $params ) { 01104 wfProfileIn( __METHOD__ ); 01105 $stat = $this->getFileStat( $params ); 01106 wfProfileOut( __METHOD__ ); 01107 return $stat ? $stat['mtime'] : false; 01108 } 01109 01113 final public function getFileSize( array $params ) { 01114 wfProfileIn( __METHOD__ ); 01115 $stat = $this->getFileStat( $params ); 01116 wfProfileOut( __METHOD__ ); 01117 return $stat ? $stat['size'] : false; 01118 } 01119 01123 final public function getFileStat( array $params ) { 01124 wfProfileIn( __METHOD__ ); 01125 $path = self::normalizeStoragePath( $params['src'] ); 01126 if ( $path === null ) { 01127 return false; // invalid storage path 01128 } 01129 $latest = !empty( $params['latest'] ); 01130 if ( isset( $this->cache[$path]['stat'] ) ) { 01131 // If we want the latest data, check that this cached 01132 // value was in fact fetched with the latest available data. 01133 if ( !$latest || $this->cache[$path]['stat']['latest'] ) { 01134 wfProfileOut( __METHOD__ ); 01135 return $this->cache[$path]['stat']; 01136 } 01137 } 01138 $stat = $this->doGetFileStat( $params ); 01139 if ( is_array( $stat ) ) { // don't cache negatives 01140 $this->trimCache(); // limit memory 01141 $this->cache[$path]['stat'] = $stat; 01142 $this->cache[$path]['stat']['latest'] = $latest; 01143 } 01144 wfProfileOut( __METHOD__ ); 01145 return $stat; 01146 } 01147 01151 abstract protected function doGetFileStat( array $params ); 01152 01156 public function getFileContents( array $params ) { 01157 wfProfileIn( __METHOD__ ); 01158 $tmpFile = $this->getLocalReference( $params ); 01159 if ( !$tmpFile ) { 01160 wfProfileOut( __METHOD__ ); 01161 return false; 01162 } 01163 wfSuppressWarnings(); 01164 $data = file_get_contents( $tmpFile->getPath() ); 01165 wfRestoreWarnings(); 01166 wfProfileOut( __METHOD__ ); 01167 return $data; 01168 } 01169 01173 final public function getFileSha1Base36( array $params ) { 01174 wfProfileIn( __METHOD__ ); 01175 $path = $params['src']; 01176 if ( isset( $this->cache[$path]['sha1'] ) ) { 01177 wfProfileOut( __METHOD__ ); 01178 return $this->cache[$path]['sha1']; 01179 } 01180 $hash = $this->doGetFileSha1Base36( $params ); 01181 if ( $hash ) { // don't cache negatives 01182 $this->trimCache(); // limit memory 01183 $this->cache[$path]['sha1'] = $hash; 01184 } 01185 wfProfileOut( __METHOD__ ); 01186 return $hash; 01187 } 01188 01192 protected function doGetFileSha1Base36( array $params ) { 01193 $fsFile = $this->getLocalReference( $params ); 01194 if ( !$fsFile ) { 01195 return false; 01196 } else { 01197 return $fsFile->getSha1Base36(); 01198 } 01199 } 01200 01204 final public function getFileProps( array $params ) { 01205 wfProfileIn( __METHOD__ ); 01206 $fsFile = $this->getLocalReference( $params ); 01207 $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps(); 01208 wfProfileOut( __METHOD__ ); 01209 return $props; 01210 } 01211 01215 public function getLocalReference( array $params ) { 01216 wfProfileIn( __METHOD__ ); 01217 $path = $params['src']; 01218 if ( isset( $this->expensiveCache[$path]['localRef'] ) ) { 01219 wfProfileOut( __METHOD__ ); 01220 return $this->expensiveCache[$path]['localRef']; 01221 } 01222 $tmpFile = $this->getLocalCopy( $params ); 01223 if ( $tmpFile ) { // don't cache negatives 01224 $this->trimExpensiveCache(); // limit memory 01225 $this->expensiveCache[$path]['localRef'] = $tmpFile; 01226 } 01227 wfProfileOut( __METHOD__ ); 01228 return $tmpFile; 01229 } 01230 01234 final public function streamFile( array $params ) { 01235 wfProfileIn( __METHOD__ ); 01236 $status = Status::newGood(); 01237 01238 $info = $this->getFileStat( $params ); 01239 if ( !$info ) { // let StreamFile handle the 404 01240 $status->fatal( 'backend-fail-notexists', $params['src'] ); 01241 } 01242 01243 // Set output buffer and HTTP headers for stream 01244 $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : array(); 01245 $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders ); 01246 if ( $res == StreamFile::NOT_MODIFIED ) { 01247 // do nothing; client cache is up to date 01248 } elseif ( $res == StreamFile::READY_STREAM ) { 01249 $status = $this->doStreamFile( $params ); 01250 } else { 01251 $status->fatal( 'backend-fail-stream', $params['src'] ); 01252 } 01253 01254 wfProfileOut( __METHOD__ ); 01255 return $status; 01256 } 01257 01261 protected function doStreamFile( array $params ) { 01262 $status = Status::newGood(); 01263 01264 $fsFile = $this->getLocalReference( $params ); 01265 if ( !$fsFile ) { 01266 $status->fatal( 'backend-fail-stream', $params['src'] ); 01267 } elseif ( !readfile( $fsFile->getPath() ) ) { 01268 $status->fatal( 'backend-fail-stream', $params['src'] ); 01269 } 01270 01271 return $status; 01272 } 01273 01277 final public function getFileList( array $params ) { 01278 list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] ); 01279 if ( $dir === null ) { // invalid storage path 01280 return null; 01281 } 01282 if ( $shard !== null ) { 01283 // File listing is confined to a single container/shard 01284 return $this->getFileListInternal( $fullCont, $dir, $params ); 01285 } else { 01286 wfDebug( __METHOD__ . ": iterating over all container shards.\n" ); 01287 // File listing spans multiple containers/shards 01288 list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] ); 01289 return new FileBackendStoreShardListIterator( $this, 01290 $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params ); 01291 } 01292 } 01293 01304 abstract public function getFileListInternal( $container, $dir, array $params ); 01305 01311 protected function supportedOperations() { 01312 return array( 01313 'store' => 'StoreFileOp', 01314 'copy' => 'CopyFileOp', 01315 'move' => 'MoveFileOp', 01316 'delete' => 'DeleteFileOp', 01317 'create' => 'CreateFileOp', 01318 'null' => 'NullFileOp' 01319 ); 01320 } 01321 01333 final public function getOperations( array $ops ) { 01334 $supportedOps = $this->supportedOperations(); 01335 01336 $performOps = array(); // array of FileOp objects 01337 // Build up ordered array of FileOps... 01338 foreach ( $ops as $operation ) { 01339 $opName = $operation['op']; 01340 if ( isset( $supportedOps[$opName] ) ) { 01341 $class = $supportedOps[$opName]; 01342 // Get params for this operation 01343 $params = $operation; 01344 // Append the FileOp class 01345 $performOps[] = new $class( $this, $params ); 01346 } else { 01347 throw new MWException( "Operation `$opName` is not supported." ); 01348 } 01349 } 01350 01351 return $performOps; 01352 } 01353 01357 protected function doOperationsInternal( array $ops, array $opts ) { 01358 wfProfileIn( __METHOD__ ); 01359 $status = Status::newGood(); 01360 01361 // Build up a list of FileOps... 01362 $performOps = $this->getOperations( $ops ); 01363 01364 // Acquire any locks as needed... 01365 if ( empty( $opts['nonLocking'] ) ) { 01366 // Build up a list of files to lock... 01367 $filesLockEx = $filesLockSh = array(); 01368 foreach ( $performOps as $fileOp ) { 01369 $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() ); 01370 $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() ); 01371 } 01372 // Optimization: if doing an EX lock anyway, don't also set an SH one 01373 $filesLockSh = array_diff( $filesLockSh, $filesLockEx ); 01374 // Get a shared lock on the parent directory of each path changed 01375 $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) ); 01376 // Try to lock those files for the scope of this function... 01377 $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status ); 01378 $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status ); 01379 if ( !$status->isOK() ) { 01380 wfProfileOut( __METHOD__ ); 01381 return $status; // abort 01382 } 01383 } 01384 01385 // Clear any cache entries (after locks acquired) 01386 $this->clearCache(); 01387 01388 // Actually attempt the operation batch... 01389 $subStatus = FileOp::attemptBatch( $performOps, $opts ); 01390 01391 // Merge errors into status fields 01392 $status->merge( $subStatus ); 01393 $status->success = $subStatus->success; // not done in merge() 01394 01395 wfProfileOut( __METHOD__ ); 01396 return $status; 01397 } 01398 01402 final public function clearCache( array $paths = null ) { 01403 if ( is_array( $paths ) ) { 01404 $paths = array_map( 'FileBackend::normalizeStoragePath', $paths ); 01405 $paths = array_filter( $paths, 'strlen' ); // remove nulls 01406 } 01407 if ( $paths === null ) { 01408 $this->cache = array(); 01409 $this->expensiveCache = array(); 01410 } else { 01411 foreach ( $paths as $path ) { 01412 unset( $this->cache[$path] ); 01413 unset( $this->expensiveCache[$path] ); 01414 } 01415 } 01416 $this->doClearCache( $paths ); 01417 } 01418 01427 protected function doClearCache( array $paths = null ) {} 01428 01434 protected function trimCache() { 01435 if ( count( $this->cache ) >= $this->maxCacheSize ) { 01436 reset( $this->cache ); 01437 unset( $this->cache[key( $this->cache )] ); 01438 } 01439 } 01440 01446 protected function trimExpensiveCache() { 01447 if ( count( $this->expensiveCache ) >= $this->maxExpensiveCacheSize ) { 01448 reset( $this->expensiveCache ); 01449 unset( $this->expensiveCache[key( $this->expensiveCache )] ); 01450 } 01451 } 01452 01460 final protected static function isValidContainerName( $container ) { 01461 // This accounts for Swift and S3 restrictions while leaving room 01462 // for things like '.xxx' (hex shard chars) or '.seg' (segments). 01463 // This disallows directory separators or traversal characters. 01464 // Note that matching strings URL encode to the same string; 01465 // in Swift, the length restriction is *after* URL encoding. 01466 return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container ); 01467 } 01468 01482 final protected function resolveStoragePath( $storagePath ) { 01483 list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath ); 01484 if ( $backend === $this->name ) { // must be for this backend 01485 $relPath = self::normalizeContainerPath( $relPath ); 01486 if ( $relPath !== null ) { 01487 // Get shard for the normalized path if this container is sharded 01488 $cShard = $this->getContainerShard( $container, $relPath ); 01489 // Validate and sanitize the relative path (backend-specific) 01490 $relPath = $this->resolveContainerPath( $container, $relPath ); 01491 if ( $relPath !== null ) { 01492 // Prepend any wiki ID prefix to the container name 01493 $container = $this->fullContainerName( $container ); 01494 if ( self::isValidContainerName( $container ) ) { 01495 // Validate and sanitize the container name (backend-specific) 01496 $container = $this->resolveContainerName( "{$container}{$cShard}" ); 01497 if ( $container !== null ) { 01498 return array( $container, $relPath, $cShard ); 01499 } 01500 } 01501 } 01502 } 01503 } 01504 return array( null, null, null ); 01505 } 01506 01516 final protected function resolveStoragePathReal( $storagePath ) { 01517 list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath ); 01518 if ( $cShard !== null ) { 01519 return array( $container, $relPath ); 01520 } 01521 return array( null, null ); 01522 } 01523 01532 final protected function getContainerShard( $container, $relPath ) { 01533 list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container ); 01534 if ( $levels == 1 || $levels == 2 ) { 01535 // Hash characters are either base 16 or 36 01536 $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]'; 01537 // Get a regex that represents the shard portion of paths. 01538 // The concatenation of the captures gives us the shard. 01539 if ( $levels === 1 ) { // 16 or 36 shards per container 01540 $hashDirRegex = '(' . $char . ')'; 01541 } else { // 256 or 1296 shards per container 01542 if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc") 01543 $hashDirRegex = $char . '/(' . $char . '{2})'; 01544 } else { // short hash dir format (e.g. "a/b/c") 01545 $hashDirRegex = '(' . $char . ')/(' . $char . ')'; 01546 } 01547 } 01548 // Allow certain directories to be above the hash dirs so as 01549 // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab"). 01550 // They must be 2+ chars to avoid any hash directory ambiguity. 01551 $m = array(); 01552 if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) { 01553 return '.' . implode( '', array_slice( $m, 1 ) ); 01554 } 01555 return null; // failed to match 01556 } 01557 return ''; // no sharding 01558 } 01559 01568 final protected function getContainerHashLevels( $container ) { 01569 if ( isset( $this->shardViaHashLevels[$container] ) ) { 01570 $config = $this->shardViaHashLevels[$container]; 01571 $hashLevels = (int)$config['levels']; 01572 if ( $hashLevels == 1 || $hashLevels == 2 ) { 01573 $hashBase = (int)$config['base']; 01574 if ( $hashBase == 16 || $hashBase == 36 ) { 01575 return array( $hashLevels, $hashBase, $config['repeat'] ); 01576 } 01577 } 01578 } 01579 return array( 0, 0, false ); // no sharding 01580 } 01581 01588 final protected function getContainerSuffixes( $container ) { 01589 $shards = array(); 01590 list( $digits, $base ) = $this->getContainerHashLevels( $container ); 01591 if ( $digits > 0 ) { 01592 $numShards = pow( $base, $digits ); 01593 for ( $index = 0; $index < $numShards; $index++ ) { 01594 $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits ); 01595 } 01596 } 01597 return $shards; 01598 } 01599 01606 final protected function fullContainerName( $container ) { 01607 if ( $this->wikiId != '' ) { 01608 return "{$this->wikiId}-$container"; 01609 } else { 01610 return $container; 01611 } 01612 } 01613 01622 protected function resolveContainerName( $container ) { 01623 return $container; 01624 } 01625 01636 protected function resolveContainerPath( $container, $relStoragePath ) { 01637 return $relStoragePath; 01638 } 01639 } 01640 01647 class FileBackendStoreShardListIterator implements Iterator { 01648 /* @var FileBackendStore */ 01649 protected $backend; 01650 /* @var Array */ 01651 protected $params; 01652 /* @var Array */ 01653 protected $shardSuffixes; 01654 protected $container; // string 01655 protected $directory; // string 01656 01657 /* @var Traversable */ 01658 protected $iter; 01659 protected $curShard = 0; // integer 01660 protected $pos = 0; // integer 01661 01669 public function __construct( 01670 FileBackendStore $backend, $container, $dir, array $suffixes, array $params 01671 ) { 01672 $this->backend = $backend; 01673 $this->container = $container; 01674 $this->directory = $dir; 01675 $this->shardSuffixes = $suffixes; 01676 $this->params = $params; 01677 } 01678 01679 public function current() { 01680 if ( is_array( $this->iter ) ) { 01681 return current( $this->iter ); 01682 } else { 01683 return $this->iter->current(); 01684 } 01685 } 01686 01687 public function key() { 01688 return $this->pos; 01689 } 01690 01691 public function next() { 01692 ++$this->pos; 01693 if ( is_array( $this->iter ) ) { 01694 next( $this->iter ); 01695 } else { 01696 $this->iter->next(); 01697 } 01698 // Find the next non-empty shard if no elements are left 01699 $this->nextShardIteratorIfNotValid(); 01700 } 01701 01707 protected function nextShardIteratorIfNotValid() { 01708 while ( !$this->valid() ) { 01709 if ( ++$this->curShard >= count( $this->shardSuffixes ) ) { 01710 break; // no more container shards 01711 } 01712 $this->setIteratorFromCurrentShard(); 01713 } 01714 } 01715 01716 protected function setIteratorFromCurrentShard() { 01717 $suffix = $this->shardSuffixes[$this->curShard]; 01718 $this->iter = $this->backend->getFileListInternal( 01719 "{$this->container}{$suffix}", $this->directory, $this->params ); 01720 } 01721 01722 public function rewind() { 01723 $this->pos = 0; 01724 $this->curShard = 0; 01725 $this->setIteratorFromCurrentShard(); 01726 // Find the next non-empty shard if this one has no elements 01727 $this->nextShardIteratorIfNotValid(); 01728 } 01729 01730 public function valid() { 01731 if ( $this->iter == null ) { 01732 return false; // some failure? 01733 } elseif ( is_array( $this->iter ) ) { 01734 return ( current( $this->iter ) !== false ); // no paths can have this value 01735 } else { 01736 return $this->iter->valid(); 01737 } 01738 } 01739 }