r112955 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r112954‎ | r112955 | r112956 >
Date:18:29, 3 March 2012
Author:aaron
Status:ok
Tags:
Comment:
Moved FileBackendStore and helper classes to their own file (no code changes). Updated AutoLoader.
Modified paths:
  • /trunk/phase3/includes/AutoLoader.php (modified) (history)
  • /trunk/phase3/includes/filerepo/backend/FileBackend.php (modified) (history)
  • /trunk/phase3/includes/filerepo/backend/FileBackendStore.php (added) (history)

Diff [purge]

Index: trunk/phase3/includes/filerepo/backend/FileBackendStore.php
@@ -0,0 +1,1060 @@
 2+<?php
 3+/**
 4+ * @file
 5+ * @ingroup FileBackend
 6+ * @author Aaron Schulz
 7+ */
 8+
 9+/**
 10+ * @brief Base class for all backends using particular storage medium.
 11+ *
 12+ * This class defines the methods as abstract that subclasses must implement.
 13+ * Outside callers should *not* use functions with "Internal" in the name.
 14+ *
 15+ * The FileBackend operations are implemented using basic functions
 16+ * such as storeInternal(), copyInternal(), deleteInternal() and the like.
 17+ * This class is also responsible for path resolution and sanitization.
 18+ *
 19+ * @ingroup FileBackend
 20+ * @since 1.19
 21+ */
 22+abstract class FileBackendStore extends FileBackend {
 23+ /** @var Array Map of paths to small (RAM/disk) cache items */
 24+ protected $cache = array(); // (storage path => key => value)
 25+ protected $maxCacheSize = 100; // integer; max paths with entries
 26+ /** @var Array Map of paths to large (RAM/disk) cache items */
 27+ protected $expensiveCache = array(); // (storage path => key => value)
 28+ protected $maxExpensiveCacheSize = 10; // integer; max paths with entries
 29+
 30+ /** @var Array Map of container names to sharding settings */
 31+ protected $shardViaHashLevels = array(); // (container name => config array)
 32+
 33+ protected $maxFileSize = 1000000000; // integer bytes (1GB)
 34+
 35+ /**
 36+ * Get the maximum allowable file size given backend
 37+ * medium restrictions and basic performance constraints.
 38+ * Do not call this function from places outside FileBackend and FileOp.
 39+ *
 40+ * @return integer Bytes
 41+ */
 42+ final public function maxFileSizeInternal() {
 43+ return $this->maxFileSize;
 44+ }
 45+
 46+ /**
 47+ * Check if a file can be created at a given storage path.
 48+ * FS backends should check if the parent directory exists and the file is writable.
 49+ * Backends using key/value stores should check if the container exists.
 50+ *
 51+ * @param $storagePath string
 52+ * @return bool
 53+ */
 54+ abstract public function isPathUsableInternal( $storagePath );
 55+
 56+ /**
 57+ * Create a file in the backend with the given contents.
 58+ * Do not call this function from places outside FileBackend and FileOp.
 59+ *
 60+ * $params include:
 61+ * content : the raw file contents
 62+ * dst : destination storage path
 63+ * overwrite : overwrite any file that exists at the destination
 64+ *
 65+ * @param $params Array
 66+ * @return Status
 67+ */
 68+ final public function createInternal( array $params ) {
 69+ wfProfileIn( __METHOD__ );
 70+ if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
 71+ $status = Status::newFatal( 'backend-fail-create', $params['dst'] );
 72+ } else {
 73+ $status = $this->doCreateInternal( $params );
 74+ $this->clearCache( array( $params['dst'] ) );
 75+ }
 76+ wfProfileOut( __METHOD__ );
 77+ return $status;
 78+ }
 79+
 80+ /**
 81+ * @see FileBackendStore::createInternal()
 82+ */
 83+ abstract protected function doCreateInternal( array $params );
 84+
 85+ /**
 86+ * Store a file into the backend from a file on disk.
 87+ * Do not call this function from places outside FileBackend and FileOp.
 88+ *
 89+ * $params include:
 90+ * src : source path on disk
 91+ * dst : destination storage path
 92+ * overwrite : overwrite any file that exists at the destination
 93+ *
 94+ * @param $params Array
 95+ * @return Status
 96+ */
 97+ final public function storeInternal( array $params ) {
 98+ wfProfileIn( __METHOD__ );
 99+ if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
 100+ $status = Status::newFatal( 'backend-fail-store', $params['dst'] );
 101+ } else {
 102+ $status = $this->doStoreInternal( $params );
 103+ $this->clearCache( array( $params['dst'] ) );
 104+ }
 105+ wfProfileOut( __METHOD__ );
 106+ return $status;
 107+ }
 108+
 109+ /**
 110+ * @see FileBackendStore::storeInternal()
 111+ */
 112+ abstract protected function doStoreInternal( array $params );
 113+
 114+ /**
 115+ * Copy a file from one storage path to another in the backend.
 116+ * Do not call this function from places outside FileBackend and FileOp.
 117+ *
 118+ * $params include:
 119+ * src : source storage path
 120+ * dst : destination storage path
 121+ * overwrite : overwrite any file that exists at the destination
 122+ *
 123+ * @param $params Array
 124+ * @return Status
 125+ */
 126+ final public function copyInternal( array $params ) {
 127+ wfProfileIn( __METHOD__ );
 128+ $status = $this->doCopyInternal( $params );
 129+ $this->clearCache( array( $params['dst'] ) );
 130+ wfProfileOut( __METHOD__ );
 131+ return $status;
 132+ }
 133+
 134+ /**
 135+ * @see FileBackendStore::copyInternal()
 136+ */
 137+ abstract protected function doCopyInternal( array $params );
 138+
 139+ /**
 140+ * Delete a file at the storage path.
 141+ * Do not call this function from places outside FileBackend and FileOp.
 142+ *
 143+ * $params include:
 144+ * src : source storage path
 145+ * ignoreMissingSource : do nothing if the source file does not exist
 146+ *
 147+ * @param $params Array
 148+ * @return Status
 149+ */
 150+ final public function deleteInternal( array $params ) {
 151+ wfProfileIn( __METHOD__ );
 152+ $status = $this->doDeleteInternal( $params );
 153+ $this->clearCache( array( $params['src'] ) );
 154+ wfProfileOut( __METHOD__ );
 155+ return $status;
 156+ }
 157+
 158+ /**
 159+ * @see FileBackendStore::deleteInternal()
 160+ */
 161+ abstract protected function doDeleteInternal( array $params );
 162+
 163+ /**
 164+ * Move a file from one storage path to another in the backend.
 165+ * Do not call this function from places outside FileBackend and FileOp.
 166+ *
 167+ * $params include:
 168+ * src : source storage path
 169+ * dst : destination storage path
 170+ * overwrite : overwrite any file that exists at the destination
 171+ *
 172+ * @param $params Array
 173+ * @return Status
 174+ */
 175+ final public function moveInternal( array $params ) {
 176+ wfProfileIn( __METHOD__ );
 177+ $status = $this->doMoveInternal( $params );
 178+ $this->clearCache( array( $params['src'], $params['dst'] ) );
 179+ wfProfileOut( __METHOD__ );
 180+ return $status;
 181+ }
 182+
 183+ /**
 184+ * @see FileBackendStore::moveInternal()
 185+ * @return Status
 186+ */
 187+ protected function doMoveInternal( array $params ) {
 188+ // Copy source to dest
 189+ $status = $this->copyInternal( $params );
 190+ if ( $status->isOK() ) {
 191+ // Delete source (only fails due to races or medium going down)
 192+ $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) );
 193+ $status->setResult( true, $status->value ); // ignore delete() errors
 194+ }
 195+ return $status;
 196+ }
 197+
 198+ /**
 199+ * @see FileBackend::concatenate()
 200+ * @return Status
 201+ */
 202+ final public function concatenate( array $params ) {
 203+ wfProfileIn( __METHOD__ );
 204+ $status = Status::newGood();
 205+
 206+ // Try to lock the source files for the scope of this function
 207+ $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
 208+ if ( $status->isOK() ) {
 209+ // Actually do the concatenation
 210+ $status->merge( $this->doConcatenate( $params ) );
 211+ }
 212+
 213+ wfProfileOut( __METHOD__ );
 214+ return $status;
 215+ }
 216+
 217+ /**
 218+ * @see FileBackendStore::concatenate()
 219+ * @return Status
 220+ */
 221+ protected function doConcatenate( array $params ) {
 222+ $status = Status::newGood();
 223+ $tmpPath = $params['dst']; // convenience
 224+
 225+ // Check that the specified temp file is valid...
 226+ wfSuppressWarnings();
 227+ $ok = ( is_file( $tmpPath ) && !filesize( $tmpPath ) );
 228+ wfRestoreWarnings();
 229+ if ( !$ok ) { // not present or not empty
 230+ $status->fatal( 'backend-fail-opentemp', $tmpPath );
 231+ return $status;
 232+ }
 233+
 234+ // Build up the temp file using the source chunks (in order)...
 235+ $tmpHandle = fopen( $tmpPath, 'ab' );
 236+ if ( $tmpHandle === false ) {
 237+ $status->fatal( 'backend-fail-opentemp', $tmpPath );
 238+ return $status;
 239+ }
 240+ foreach ( $params['srcs'] as $virtualSource ) {
 241+ // Get a local FS version of the chunk
 242+ $tmpFile = $this->getLocalReference( array( 'src' => $virtualSource ) );
 243+ if ( !$tmpFile ) {
 244+ $status->fatal( 'backend-fail-read', $virtualSource );
 245+ return $status;
 246+ }
 247+ // Get a handle to the local FS version
 248+ $sourceHandle = fopen( $tmpFile->getPath(), 'r' );
 249+ if ( $sourceHandle === false ) {
 250+ fclose( $tmpHandle );
 251+ $status->fatal( 'backend-fail-read', $virtualSource );
 252+ return $status;
 253+ }
 254+ // Append chunk to file (pass chunk size to avoid magic quotes)
 255+ if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
 256+ fclose( $sourceHandle );
 257+ fclose( $tmpHandle );
 258+ $status->fatal( 'backend-fail-writetemp', $tmpPath );
 259+ return $status;
 260+ }
 261+ fclose( $sourceHandle );
 262+ }
 263+ if ( !fclose( $tmpHandle ) ) {
 264+ $status->fatal( 'backend-fail-closetemp', $tmpPath );
 265+ return $status;
 266+ }
 267+
 268+ clearstatcache(); // temp file changed
 269+
 270+ return $status;
 271+ }
 272+
 273+ /**
 274+ * @see FileBackend::doPrepare()
 275+ * @return Status
 276+ */
 277+ final protected function doPrepare( array $params ) {
 278+ wfProfileIn( __METHOD__ );
 279+
 280+ $status = Status::newGood();
 281+ list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
 282+ if ( $dir === null ) {
 283+ $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
 284+ wfProfileOut( __METHOD__ );
 285+ return $status; // invalid storage path
 286+ }
 287+
 288+ if ( $shard !== null ) { // confined to a single container/shard
 289+ $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
 290+ } else { // directory is on several shards
 291+ wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
 292+ list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
 293+ foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
 294+ $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
 295+ }
 296+ }
 297+
 298+ wfProfileOut( __METHOD__ );
 299+ return $status;
 300+ }
 301+
 302+ /**
 303+ * @see FileBackendStore::doPrepare()
 304+ * @return Status
 305+ */
 306+ protected function doPrepareInternal( $container, $dir, array $params ) {
 307+ return Status::newGood();
 308+ }
 309+
 310+ /**
 311+ * @see FileBackend::doSecure()
 312+ * @return Status
 313+ */
 314+ final protected function doSecure( array $params ) {
 315+ wfProfileIn( __METHOD__ );
 316+ $status = Status::newGood();
 317+
 318+ list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
 319+ if ( $dir === null ) {
 320+ $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
 321+ wfProfileOut( __METHOD__ );
 322+ return $status; // invalid storage path
 323+ }
 324+
 325+ if ( $shard !== null ) { // confined to a single container/shard
 326+ $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
 327+ } else { // directory is on several shards
 328+ wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
 329+ list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
 330+ foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
 331+ $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
 332+ }
 333+ }
 334+
 335+ wfProfileOut( __METHOD__ );
 336+ return $status;
 337+ }
 338+
 339+ /**
 340+ * @see FileBackendStore::doSecure()
 341+ * @return Status
 342+ */
 343+ protected function doSecureInternal( $container, $dir, array $params ) {
 344+ return Status::newGood();
 345+ }
 346+
 347+ /**
 348+ * @see FileBackend::doClean()
 349+ * @return Status
 350+ */
 351+ final protected function doClean( array $params ) {
 352+ wfProfileIn( __METHOD__ );
 353+ $status = Status::newGood();
 354+
 355+ list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
 356+ if ( $dir === null ) {
 357+ $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
 358+ wfProfileOut( __METHOD__ );
 359+ return $status; // invalid storage path
 360+ }
 361+
 362+ // Attempt to lock this directory...
 363+ $filesLockEx = array( $params['dir'] );
 364+ $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
 365+ if ( !$status->isOK() ) {
 366+ wfProfileOut( __METHOD__ );
 367+ return $status; // abort
 368+ }
 369+
 370+ if ( $shard !== null ) { // confined to a single container/shard
 371+ $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
 372+ } else { // directory is on several shards
 373+ wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
 374+ list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
 375+ foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
 376+ $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
 377+ }
 378+ }
 379+
 380+ wfProfileOut( __METHOD__ );
 381+ return $status;
 382+ }
 383+
 384+ /**
 385+ * @see FileBackendStore::doClean()
 386+ * @return Status
 387+ */
 388+ protected function doCleanInternal( $container, $dir, array $params ) {
 389+ return Status::newGood();
 390+ }
 391+
 392+ /**
 393+ * @see FileBackend::fileExists()
 394+ * @return bool|null
 395+ */
 396+ final public function fileExists( array $params ) {
 397+ wfProfileIn( __METHOD__ );
 398+ $stat = $this->getFileStat( $params );
 399+ wfProfileOut( __METHOD__ );
 400+ return ( $stat === null ) ? null : (bool)$stat; // null => failure
 401+ }
 402+
 403+ /**
 404+ * @see FileBackend::getFileTimestamp()
 405+ * @return bool
 406+ */
 407+ final public function getFileTimestamp( array $params ) {
 408+ wfProfileIn( __METHOD__ );
 409+ $stat = $this->getFileStat( $params );
 410+ wfProfileOut( __METHOD__ );
 411+ return $stat ? $stat['mtime'] : false;
 412+ }
 413+
 414+ /**
 415+ * @see FileBackend::getFileSize()
 416+ * @return bool
 417+ */
 418+ final public function getFileSize( array $params ) {
 419+ wfProfileIn( __METHOD__ );
 420+ $stat = $this->getFileStat( $params );
 421+ wfProfileOut( __METHOD__ );
 422+ return $stat ? $stat['size'] : false;
 423+ }
 424+
 425+ /**
 426+ * @see FileBackend::getFileStat()
 427+ * @return bool
 428+ */
 429+ final public function getFileStat( array $params ) {
 430+ wfProfileIn( __METHOD__ );
 431+ $path = self::normalizeStoragePath( $params['src'] );
 432+ if ( $path === null ) {
 433+ wfProfileOut( __METHOD__ );
 434+ return false; // invalid storage path
 435+ }
 436+ $latest = !empty( $params['latest'] );
 437+ if ( isset( $this->cache[$path]['stat'] ) ) {
 438+ // If we want the latest data, check that this cached
 439+ // value was in fact fetched with the latest available data.
 440+ if ( !$latest || $this->cache[$path]['stat']['latest'] ) {
 441+ wfProfileOut( __METHOD__ );
 442+ return $this->cache[$path]['stat'];
 443+ }
 444+ }
 445+ wfProfileIn( __METHOD__ . '-miss' );
 446+ $stat = $this->doGetFileStat( $params );
 447+ wfProfileOut( __METHOD__ . '-miss' );
 448+ if ( is_array( $stat ) ) { // don't cache negatives
 449+ $this->trimCache(); // limit memory
 450+ $this->cache[$path]['stat'] = $stat;
 451+ $this->cache[$path]['stat']['latest'] = $latest;
 452+ }
 453+ wfProfileOut( __METHOD__ );
 454+ return $stat;
 455+ }
 456+
 457+ /**
 458+ * @see FileBackendStore::getFileStat()
 459+ */
 460+ abstract protected function doGetFileStat( array $params );
 461+
 462+ /**
 463+ * @see FileBackend::getFileContents()
 464+ * @return bool|string
 465+ */
 466+ public function getFileContents( array $params ) {
 467+ wfProfileIn( __METHOD__ );
 468+ $tmpFile = $this->getLocalReference( $params );
 469+ if ( !$tmpFile ) {
 470+ wfProfileOut( __METHOD__ );
 471+ return false;
 472+ }
 473+ wfSuppressWarnings();
 474+ $data = file_get_contents( $tmpFile->getPath() );
 475+ wfRestoreWarnings();
 476+ wfProfileOut( __METHOD__ );
 477+ return $data;
 478+ }
 479+
 480+ /**
 481+ * @see FileBackend::getFileSha1Base36()
 482+ * @return bool|string
 483+ */
 484+ final public function getFileSha1Base36( array $params ) {
 485+ wfProfileIn( __METHOD__ );
 486+ $path = $params['src'];
 487+ if ( isset( $this->cache[$path]['sha1'] ) ) {
 488+ wfProfileOut( __METHOD__ );
 489+ return $this->cache[$path]['sha1'];
 490+ }
 491+ wfProfileIn( __METHOD__ . '-miss' );
 492+ $hash = $this->doGetFileSha1Base36( $params );
 493+ wfProfileOut( __METHOD__ . '-miss' );
 494+ if ( $hash ) { // don't cache negatives
 495+ $this->trimCache(); // limit memory
 496+ $this->cache[$path]['sha1'] = $hash;
 497+ }
 498+ wfProfileOut( __METHOD__ );
 499+ return $hash;
 500+ }
 501+
 502+ /**
 503+ * @see FileBackendStore::getFileSha1Base36()
 504+ * @return bool
 505+ */
 506+ protected function doGetFileSha1Base36( array $params ) {
 507+ $fsFile = $this->getLocalReference( $params );
 508+ if ( !$fsFile ) {
 509+ return false;
 510+ } else {
 511+ return $fsFile->getSha1Base36();
 512+ }
 513+ }
 514+
 515+ /**
 516+ * @see FileBackend::getFileProps()
 517+ * @return Array
 518+ */
 519+ final public function getFileProps( array $params ) {
 520+ wfProfileIn( __METHOD__ );
 521+ $fsFile = $this->getLocalReference( $params );
 522+ $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
 523+ wfProfileOut( __METHOD__ );
 524+ return $props;
 525+ }
 526+
 527+ /**
 528+ * @see FileBackend::getLocalReference()
 529+ * @return TempFSFile|null
 530+ */
 531+ public function getLocalReference( array $params ) {
 532+ wfProfileIn( __METHOD__ );
 533+ $path = $params['src'];
 534+ if ( isset( $this->expensiveCache[$path]['localRef'] ) ) {
 535+ wfProfileOut( __METHOD__ );
 536+ return $this->expensiveCache[$path]['localRef'];
 537+ }
 538+ $tmpFile = $this->getLocalCopy( $params );
 539+ if ( $tmpFile ) { // don't cache negatives
 540+ $this->trimExpensiveCache(); // limit memory
 541+ $this->expensiveCache[$path]['localRef'] = $tmpFile;
 542+ }
 543+ wfProfileOut( __METHOD__ );
 544+ return $tmpFile;
 545+ }
 546+
 547+ /**
 548+ * @see FileBackend::streamFile()
 549+ * @return Status
 550+ */
 551+ final public function streamFile( array $params ) {
 552+ wfProfileIn( __METHOD__ );
 553+ $status = Status::newGood();
 554+
 555+ $info = $this->getFileStat( $params );
 556+ if ( !$info ) { // let StreamFile handle the 404
 557+ $status->fatal( 'backend-fail-notexists', $params['src'] );
 558+ }
 559+
 560+ // Set output buffer and HTTP headers for stream
 561+ $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : array();
 562+ $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
 563+ if ( $res == StreamFile::NOT_MODIFIED ) {
 564+ // do nothing; client cache is up to date
 565+ } elseif ( $res == StreamFile::READY_STREAM ) {
 566+ wfProfileIn( __METHOD__ . '-send' );
 567+ $status = $this->doStreamFile( $params );
 568+ wfProfileOut( __METHOD__ . '-send' );
 569+ } else {
 570+ $status->fatal( 'backend-fail-stream', $params['src'] );
 571+ }
 572+
 573+ wfProfileOut( __METHOD__ );
 574+ return $status;
 575+ }
 576+
 577+ /**
 578+ * @see FileBackendStore::streamFile()
 579+ * @return Status
 580+ */
 581+ protected function doStreamFile( array $params ) {
 582+ $status = Status::newGood();
 583+
 584+ $fsFile = $this->getLocalReference( $params );
 585+ if ( !$fsFile ) {
 586+ $status->fatal( 'backend-fail-stream', $params['src'] );
 587+ } elseif ( !readfile( $fsFile->getPath() ) ) {
 588+ $status->fatal( 'backend-fail-stream', $params['src'] );
 589+ }
 590+
 591+ return $status;
 592+ }
 593+
 594+ /**
 595+ * @copydoc FileBackend::getFileList()
 596+ * @return Array|null|Traversable
 597+ */
 598+ final public function getFileList( array $params ) {
 599+ list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
 600+ if ( $dir === null ) { // invalid storage path
 601+ return null;
 602+ }
 603+ if ( $shard !== null ) {
 604+ // File listing is confined to a single container/shard
 605+ return $this->getFileListInternal( $fullCont, $dir, $params );
 606+ } else {
 607+ wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
 608+ // File listing spans multiple containers/shards
 609+ list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
 610+ return new FileBackendStoreShardListIterator( $this,
 611+ $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
 612+ }
 613+ }
 614+
 615+ /**
 616+ * Do not call this function from places outside FileBackend
 617+ *
 618+ * @see FileBackendStore::getFileList()
 619+ *
 620+ * @param $container string Resolved container name
 621+ * @param $dir string Resolved path relative to container
 622+ * @param $params Array
 623+ * @return Traversable|Array|null
 624+ */
 625+ abstract public function getFileListInternal( $container, $dir, array $params );
 626+
 627+ /**
 628+ * Get the list of supported operations and their corresponding FileOp classes.
 629+ *
 630+ * @return Array
 631+ */
 632+ protected function supportedOperations() {
 633+ return array(
 634+ 'store' => 'StoreFileOp',
 635+ 'copy' => 'CopyFileOp',
 636+ 'move' => 'MoveFileOp',
 637+ 'delete' => 'DeleteFileOp',
 638+ 'create' => 'CreateFileOp',
 639+ 'null' => 'NullFileOp'
 640+ );
 641+ }
 642+
 643+ /**
 644+ * Return a list of FileOp objects from a list of operations.
 645+ * Do not call this function from places outside FileBackend.
 646+ *
 647+ * The result must have the same number of items as the input.
 648+ * An exception is thrown if an unsupported operation is requested.
 649+ *
 650+ * @param $ops Array Same format as doOperations()
 651+ * @return Array List of FileOp objects
 652+ * @throws MWException
 653+ */
 654+ final public function getOperations( array $ops ) {
 655+ $supportedOps = $this->supportedOperations();
 656+
 657+ $performOps = array(); // array of FileOp objects
 658+ // Build up ordered array of FileOps...
 659+ foreach ( $ops as $operation ) {
 660+ $opName = $operation['op'];
 661+ if ( isset( $supportedOps[$opName] ) ) {
 662+ $class = $supportedOps[$opName];
 663+ // Get params for this operation
 664+ $params = $operation;
 665+ // Append the FileOp class
 666+ $performOps[] = new $class( $this, $params );
 667+ } else {
 668+ throw new MWException( "Operation `$opName` is not supported." );
 669+ }
 670+ }
 671+
 672+ return $performOps;
 673+ }
 674+
 675+ /**
 676+ * @see FileBackend::doOperationsInternal()
 677+ * @return Status
 678+ */
 679+ protected function doOperationsInternal( array $ops, array $opts ) {
 680+ wfProfileIn( __METHOD__ );
 681+ $status = Status::newGood();
 682+
 683+ // Build up a list of FileOps...
 684+ $performOps = $this->getOperations( $ops );
 685+
 686+ // Acquire any locks as needed...
 687+ if ( empty( $opts['nonLocking'] ) ) {
 688+ // Build up a list of files to lock...
 689+ $filesLockEx = $filesLockSh = array();
 690+ foreach ( $performOps as $fileOp ) {
 691+ $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() );
 692+ $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() );
 693+ }
 694+ // Optimization: if doing an EX lock anyway, don't also set an SH one
 695+ $filesLockSh = array_diff( $filesLockSh, $filesLockEx );
 696+ // Get a shared lock on the parent directory of each path changed
 697+ $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
 698+ // Try to lock those files for the scope of this function...
 699+ $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
 700+ $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
 701+ if ( !$status->isOK() ) {
 702+ wfProfileOut( __METHOD__ );
 703+ return $status; // abort
 704+ }
 705+ }
 706+
 707+ // Clear any cache entries (after locks acquired)
 708+ $this->clearCache();
 709+
 710+ // Actually attempt the operation batch...
 711+ $subStatus = FileOp::attemptBatch( $performOps, $opts );
 712+
 713+ // Merge errors into status fields
 714+ $status->merge( $subStatus );
 715+ $status->success = $subStatus->success; // not done in merge()
 716+
 717+ wfProfileOut( __METHOD__ );
 718+ return $status;
 719+ }
 720+
 721+ /**
 722+ * @see FileBackend::clearCache()
 723+ */
 724+ final public function clearCache( array $paths = null ) {
 725+ if ( is_array( $paths ) ) {
 726+ $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
 727+ $paths = array_filter( $paths, 'strlen' ); // remove nulls
 728+ }
 729+ if ( $paths === null ) {
 730+ $this->cache = array();
 731+ $this->expensiveCache = array();
 732+ } else {
 733+ foreach ( $paths as $path ) {
 734+ unset( $this->cache[$path] );
 735+ unset( $this->expensiveCache[$path] );
 736+ }
 737+ }
 738+ $this->doClearCache( $paths );
 739+ }
 740+
 741+ /**
 742+ * Clears any additional stat caches for storage paths
 743+ *
 744+ * @see FileBackend::clearCache()
 745+ *
 746+ * @param $paths Array Storage paths (optional)
 747+ * @return void
 748+ */
 749+ protected function doClearCache( array $paths = null ) {}
 750+
 751+ /**
 752+ * Prune the inexpensive cache if it is too big to add an item
 753+ *
 754+ * @return void
 755+ */
 756+ protected function trimCache() {
 757+ if ( count( $this->cache ) >= $this->maxCacheSize ) {
 758+ reset( $this->cache );
 759+ unset( $this->cache[key( $this->cache )] );
 760+ }
 761+ }
 762+
 763+ /**
 764+ * Prune the expensive cache if it is too big to add an item
 765+ *
 766+ * @return void
 767+ */
 768+ protected function trimExpensiveCache() {
 769+ if ( count( $this->expensiveCache ) >= $this->maxExpensiveCacheSize ) {
 770+ reset( $this->expensiveCache );
 771+ unset( $this->expensiveCache[key( $this->expensiveCache )] );
 772+ }
 773+ }
 774+
 775+ /**
 776+ * Check if a container name is valid.
 777+ * This checks for for length and illegal characters.
 778+ *
 779+ * @param $container string
 780+ * @return bool
 781+ */
 782+ final protected static function isValidContainerName( $container ) {
 783+ // This accounts for Swift and S3 restrictions while leaving room
 784+ // for things like '.xxx' (hex shard chars) or '.seg' (segments).
 785+ // This disallows directory separators or traversal characters.
 786+ // Note that matching strings URL encode to the same string;
 787+ // in Swift, the length restriction is *after* URL encoding.
 788+ return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container );
 789+ }
 790+
 791+ /**
 792+ * Splits a storage path into an internal container name,
 793+ * an internal relative file name, and a container shard suffix.
 794+ * Any shard suffix is already appended to the internal container name.
 795+ * This also checks that the storage path is valid and within this backend.
 796+ *
 797+ * If the container is sharded but a suffix could not be determined,
 798+ * this means that the path can only refer to a directory and can only
 799+ * be scanned by looking in all the container shards.
 800+ *
 801+ * @param $storagePath string
 802+ * @return Array (container, path, container suffix) or (null, null, null) if invalid
 803+ */
 804+ final protected function resolveStoragePath( $storagePath ) {
 805+ list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
 806+ if ( $backend === $this->name ) { // must be for this backend
 807+ $relPath = self::normalizeContainerPath( $relPath );
 808+ if ( $relPath !== null ) {
 809+ // Get shard for the normalized path if this container is sharded
 810+ $cShard = $this->getContainerShard( $container, $relPath );
 811+ // Validate and sanitize the relative path (backend-specific)
 812+ $relPath = $this->resolveContainerPath( $container, $relPath );
 813+ if ( $relPath !== null ) {
 814+ // Prepend any wiki ID prefix to the container name
 815+ $container = $this->fullContainerName( $container );
 816+ if ( self::isValidContainerName( $container ) ) {
 817+ // Validate and sanitize the container name (backend-specific)
 818+ $container = $this->resolveContainerName( "{$container}{$cShard}" );
 819+ if ( $container !== null ) {
 820+ return array( $container, $relPath, $cShard );
 821+ }
 822+ }
 823+ }
 824+ }
 825+ }
 826+ return array( null, null, null );
 827+ }
 828+
 829+ /**
 830+ * Like resolveStoragePath() except null values are returned if
 831+ * the container is sharded and the shard could not be determined.
 832+ *
 833+ * @see FileBackendStore::resolveStoragePath()
 834+ *
 835+ * @param $storagePath string
 836+ * @return Array (container, path) or (null, null) if invalid
 837+ */
 838+ final protected function resolveStoragePathReal( $storagePath ) {
 839+ list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
 840+ if ( $cShard !== null ) {
 841+ return array( $container, $relPath );
 842+ }
 843+ return array( null, null );
 844+ }
 845+
 846+ /**
 847+ * Get the container name shard suffix for a given path.
 848+ * Any empty suffix means the container is not sharded.
 849+ *
 850+ * @param $container string Container name
 851+ * @param $relStoragePath string Storage path relative to the container
 852+ * @return string|null Returns null if shard could not be determined
 853+ */
 854+ final protected function getContainerShard( $container, $relPath ) {
 855+ list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
 856+ if ( $levels == 1 || $levels == 2 ) {
 857+ // Hash characters are either base 16 or 36
 858+ $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
 859+ // Get a regex that represents the shard portion of paths.
 860+ // The concatenation of the captures gives us the shard.
 861+ if ( $levels === 1 ) { // 16 or 36 shards per container
 862+ $hashDirRegex = '(' . $char . ')';
 863+ } else { // 256 or 1296 shards per container
 864+ if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
 865+ $hashDirRegex = $char . '/(' . $char . '{2})';
 866+ } else { // short hash dir format (e.g. "a/b/c")
 867+ $hashDirRegex = '(' . $char . ')/(' . $char . ')';
 868+ }
 869+ }
 870+ // Allow certain directories to be above the hash dirs so as
 871+ // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
 872+ // They must be 2+ chars to avoid any hash directory ambiguity.
 873+ $m = array();
 874+ if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
 875+ return '.' . implode( '', array_slice( $m, 1 ) );
 876+ }
 877+ return null; // failed to match
 878+ }
 879+ return ''; // no sharding
 880+ }
 881+
 882+ /**
 883+ * Get the sharding config for a container.
 884+ * If greater than 0, then all file storage paths within
 885+ * the container are required to be hashed accordingly.
 886+ *
 887+ * @param $container string
 888+ * @return Array (integer levels, integer base, repeat flag) or (0, 0, false)
 889+ */
 890+ final protected function getContainerHashLevels( $container ) {
 891+ if ( isset( $this->shardViaHashLevels[$container] ) ) {
 892+ $config = $this->shardViaHashLevels[$container];
 893+ $hashLevels = (int)$config['levels'];
 894+ if ( $hashLevels == 1 || $hashLevels == 2 ) {
 895+ $hashBase = (int)$config['base'];
 896+ if ( $hashBase == 16 || $hashBase == 36 ) {
 897+ return array( $hashLevels, $hashBase, $config['repeat'] );
 898+ }
 899+ }
 900+ }
 901+ return array( 0, 0, false ); // no sharding
 902+ }
 903+
 904+ /**
 905+ * Get a list of full container shard suffixes for a container
 906+ *
 907+ * @param $container string
 908+ * @return Array
 909+ */
 910+ final protected function getContainerSuffixes( $container ) {
 911+ $shards = array();
 912+ list( $digits, $base ) = $this->getContainerHashLevels( $container );
 913+ if ( $digits > 0 ) {
 914+ $numShards = pow( $base, $digits );
 915+ for ( $index = 0; $index < $numShards; $index++ ) {
 916+ $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits );
 917+ }
 918+ }
 919+ return $shards;
 920+ }
 921+
 922+ /**
 923+ * Get the full container name, including the wiki ID prefix
 924+ *
 925+ * @param $container string
 926+ * @return string
 927+ */
 928+ final protected function fullContainerName( $container ) {
 929+ if ( $this->wikiId != '' ) {
 930+ return "{$this->wikiId}-$container";
 931+ } else {
 932+ return $container;
 933+ }
 934+ }
 935+
 936+ /**
 937+ * Resolve a container name, checking if it's allowed by the backend.
 938+ * This is intended for internal use, such as encoding illegal chars.
 939+ * Subclasses can override this to be more restrictive.
 940+ *
 941+ * @param $container string
 942+ * @return string|null
 943+ */
 944+ protected function resolveContainerName( $container ) {
 945+ return $container;
 946+ }
 947+
 948+ /**
 949+ * Resolve a relative storage path, checking if it's allowed by the backend.
 950+ * This is intended for internal use, such as encoding illegal chars or perhaps
 951+ * getting absolute paths (e.g. FS based backends). Note that the relative path
 952+ * may be the empty string (e.g. the path is simply to the container).
 953+ *
 954+ * @param $container string Container name
 955+ * @param $relStoragePath string Storage path relative to the container
 956+ * @return string|null Path or null if not valid
 957+ */
 958+ protected function resolveContainerPath( $container, $relStoragePath ) {
 959+ return $relStoragePath;
 960+ }
 961+}
 962+
 963+/**
 964+ * FileBackendStore helper function to handle file listings that span container shards.
 965+ * Do not use this class from places outside of FileBackendStore.
 966+ *
 967+ * @ingroup FileBackend
 968+ */
 969+class FileBackendStoreShardListIterator implements Iterator {
 970+ /* @var FileBackendStore */
 971+ protected $backend;
 972+ /* @var Array */
 973+ protected $params;
 974+ /* @var Array */
 975+ protected $shardSuffixes;
 976+ protected $container; // string
 977+ protected $directory; // string
 978+
 979+ /* @var Traversable */
 980+ protected $iter;
 981+ protected $curShard = 0; // integer
 982+ protected $pos = 0; // integer
 983+
 984+ /**
 985+ * @param $backend FileBackendStore
 986+ * @param $container string Full storage container name
 987+ * @param $dir string Storage directory relative to container
 988+ * @param $suffixes Array List of container shard suffixes
 989+ * @param $params Array
 990+ */
 991+ public function __construct(
 992+ FileBackendStore $backend, $container, $dir, array $suffixes, array $params
 993+ ) {
 994+ $this->backend = $backend;
 995+ $this->container = $container;
 996+ $this->directory = $dir;
 997+ $this->shardSuffixes = $suffixes;
 998+ $this->params = $params;
 999+ }
 1000+
 1001+ public function current() {
 1002+ if ( is_array( $this->iter ) ) {
 1003+ return current( $this->iter );
 1004+ } else {
 1005+ return $this->iter->current();
 1006+ }
 1007+ }
 1008+
 1009+ public function key() {
 1010+ return $this->pos;
 1011+ }
 1012+
 1013+ public function next() {
 1014+ ++$this->pos;
 1015+ if ( is_array( $this->iter ) ) {
 1016+ next( $this->iter );
 1017+ } else {
 1018+ $this->iter->next();
 1019+ }
 1020+ // Find the next non-empty shard if no elements are left
 1021+ $this->nextShardIteratorIfNotValid();
 1022+ }
 1023+
 1024+ /**
 1025+ * If the iterator for this container shard is out of items,
 1026+ * then move on to the next container that has items.
 1027+ * If there are none, then it advances to the last container.
 1028+ */
 1029+ protected function nextShardIteratorIfNotValid() {
 1030+ while ( !$this->valid() ) {
 1031+ if ( ++$this->curShard >= count( $this->shardSuffixes ) ) {
 1032+ break; // no more container shards
 1033+ }
 1034+ $this->setIteratorFromCurrentShard();
 1035+ }
 1036+ }
 1037+
 1038+ protected function setIteratorFromCurrentShard() {
 1039+ $suffix = $this->shardSuffixes[$this->curShard];
 1040+ $this->iter = $this->backend->getFileListInternal(
 1041+ "{$this->container}{$suffix}", $this->directory, $this->params );
 1042+ }
 1043+
 1044+ public function rewind() {
 1045+ $this->pos = 0;
 1046+ $this->curShard = 0;
 1047+ $this->setIteratorFromCurrentShard();
 1048+ // Find the next non-empty shard if this one has no elements
 1049+ $this->nextShardIteratorIfNotValid();
 1050+ }
 1051+
 1052+ public function valid() {
 1053+ if ( $this->iter == null ) {
 1054+ return false; // some failure?
 1055+ } elseif ( is_array( $this->iter ) ) {
 1056+ return ( current( $this->iter ) !== false ); // no paths can have this value
 1057+ } else {
 1058+ return $this->iter->valid();
 1059+ }
 1060+ }
 1061+}
Property changes on: trunk/phase3/includes/filerepo/backend/FileBackendStore.php
___________________________________________________________________
Added: svn:eol-style
11062 + native
Index: trunk/phase3/includes/filerepo/backend/FileBackend.php
@@ -713,1057 +713,3 @@
714714 return strtolower( $i ? substr( $path, $i + 1 ) : '' );
715715 }
716716 }
717 -
718 -/**
719 - * @brief Base class for all backends using particular storage medium.
720 - *
721 - * This class defines the methods as abstract that subclasses must implement.
722 - * Outside callers should *not* use functions with "Internal" in the name.
723 - *
724 - * The FileBackend operations are implemented using basic functions
725 - * such as storeInternal(), copyInternal(), deleteInternal() and the like.
726 - * This class is also responsible for path resolution and sanitization.
727 - *
728 - * @ingroup FileBackend
729 - * @since 1.19
730 - */
731 -abstract class FileBackendStore extends FileBackend {
732 - /** @var Array Map of paths to small (RAM/disk) cache items */
733 - protected $cache = array(); // (storage path => key => value)
734 - protected $maxCacheSize = 100; // integer; max paths with entries
735 - /** @var Array Map of paths to large (RAM/disk) cache items */
736 - protected $expensiveCache = array(); // (storage path => key => value)
737 - protected $maxExpensiveCacheSize = 10; // integer; max paths with entries
738 -
739 - /** @var Array Map of container names to sharding settings */
740 - protected $shardViaHashLevels = array(); // (container name => config array)
741 -
742 - protected $maxFileSize = 1000000000; // integer bytes (1GB)
743 -
744 - /**
745 - * Get the maximum allowable file size given backend
746 - * medium restrictions and basic performance constraints.
747 - * Do not call this function from places outside FileBackend and FileOp.
748 - *
749 - * @return integer Bytes
750 - */
751 - final public function maxFileSizeInternal() {
752 - return $this->maxFileSize;
753 - }
754 -
755 - /**
756 - * Check if a file can be created at a given storage path.
757 - * FS backends should check if the parent directory exists and the file is writable.
758 - * Backends using key/value stores should check if the container exists.
759 - *
760 - * @param $storagePath string
761 - * @return bool
762 - */
763 - abstract public function isPathUsableInternal( $storagePath );
764 -
765 - /**
766 - * Create a file in the backend with the given contents.
767 - * Do not call this function from places outside FileBackend and FileOp.
768 - *
769 - * $params include:
770 - * content : the raw file contents
771 - * dst : destination storage path
772 - * overwrite : overwrite any file that exists at the destination
773 - *
774 - * @param $params Array
775 - * @return Status
776 - */
777 - final public function createInternal( array $params ) {
778 - wfProfileIn( __METHOD__ );
779 - if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
780 - $status = Status::newFatal( 'backend-fail-create', $params['dst'] );
781 - } else {
782 - $status = $this->doCreateInternal( $params );
783 - $this->clearCache( array( $params['dst'] ) );
784 - }
785 - wfProfileOut( __METHOD__ );
786 - return $status;
787 - }
788 -
789 - /**
790 - * @see FileBackendStore::createInternal()
791 - */
792 - abstract protected function doCreateInternal( array $params );
793 -
794 - /**
795 - * Store a file into the backend from a file on disk.
796 - * Do not call this function from places outside FileBackend and FileOp.
797 - *
798 - * $params include:
799 - * src : source path on disk
800 - * dst : destination storage path
801 - * overwrite : overwrite any file that exists at the destination
802 - *
803 - * @param $params Array
804 - * @return Status
805 - */
806 - final public function storeInternal( array $params ) {
807 - wfProfileIn( __METHOD__ );
808 - if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
809 - $status = Status::newFatal( 'backend-fail-store', $params['dst'] );
810 - } else {
811 - $status = $this->doStoreInternal( $params );
812 - $this->clearCache( array( $params['dst'] ) );
813 - }
814 - wfProfileOut( __METHOD__ );
815 - return $status;
816 - }
817 -
818 - /**
819 - * @see FileBackendStore::storeInternal()
820 - */
821 - abstract protected function doStoreInternal( array $params );
822 -
823 - /**
824 - * Copy a file from one storage path to another in the backend.
825 - * Do not call this function from places outside FileBackend and FileOp.
826 - *
827 - * $params include:
828 - * src : source storage path
829 - * dst : destination storage path
830 - * overwrite : overwrite any file that exists at the destination
831 - *
832 - * @param $params Array
833 - * @return Status
834 - */
835 - final public function copyInternal( array $params ) {
836 - wfProfileIn( __METHOD__ );
837 - $status = $this->doCopyInternal( $params );
838 - $this->clearCache( array( $params['dst'] ) );
839 - wfProfileOut( __METHOD__ );
840 - return $status;
841 - }
842 -
843 - /**
844 - * @see FileBackendStore::copyInternal()
845 - */
846 - abstract protected function doCopyInternal( array $params );
847 -
848 - /**
849 - * Delete a file at the storage path.
850 - * Do not call this function from places outside FileBackend and FileOp.
851 - *
852 - * $params include:
853 - * src : source storage path
854 - * ignoreMissingSource : do nothing if the source file does not exist
855 - *
856 - * @param $params Array
857 - * @return Status
858 - */
859 - final public function deleteInternal( array $params ) {
860 - wfProfileIn( __METHOD__ );
861 - $status = $this->doDeleteInternal( $params );
862 - $this->clearCache( array( $params['src'] ) );
863 - wfProfileOut( __METHOD__ );
864 - return $status;
865 - }
866 -
867 - /**
868 - * @see FileBackendStore::deleteInternal()
869 - */
870 - abstract protected function doDeleteInternal( array $params );
871 -
872 - /**
873 - * Move a file from one storage path to another in the backend.
874 - * Do not call this function from places outside FileBackend and FileOp.
875 - *
876 - * $params include:
877 - * src : source storage path
878 - * dst : destination storage path
879 - * overwrite : overwrite any file that exists at the destination
880 - *
881 - * @param $params Array
882 - * @return Status
883 - */
884 - final public function moveInternal( array $params ) {
885 - wfProfileIn( __METHOD__ );
886 - $status = $this->doMoveInternal( $params );
887 - $this->clearCache( array( $params['src'], $params['dst'] ) );
888 - wfProfileOut( __METHOD__ );
889 - return $status;
890 - }
891 -
892 - /**
893 - * @see FileBackendStore::moveInternal()
894 - * @return Status
895 - */
896 - protected function doMoveInternal( array $params ) {
897 - // Copy source to dest
898 - $status = $this->copyInternal( $params );
899 - if ( $status->isOK() ) {
900 - // Delete source (only fails due to races or medium going down)
901 - $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) );
902 - $status->setResult( true, $status->value ); // ignore delete() errors
903 - }
904 - return $status;
905 - }
906 -
907 - /**
908 - * @see FileBackend::concatenate()
909 - * @return Status
910 - */
911 - final public function concatenate( array $params ) {
912 - wfProfileIn( __METHOD__ );
913 - $status = Status::newGood();
914 -
915 - // Try to lock the source files for the scope of this function
916 - $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
917 - if ( $status->isOK() ) {
918 - // Actually do the concatenation
919 - $status->merge( $this->doConcatenate( $params ) );
920 - }
921 -
922 - wfProfileOut( __METHOD__ );
923 - return $status;
924 - }
925 -
926 - /**
927 - * @see FileBackendStore::concatenate()
928 - * @return Status
929 - */
930 - protected function doConcatenate( array $params ) {
931 - $status = Status::newGood();
932 - $tmpPath = $params['dst']; // convenience
933 -
934 - // Check that the specified temp file is valid...
935 - wfSuppressWarnings();
936 - $ok = ( is_file( $tmpPath ) && !filesize( $tmpPath ) );
937 - wfRestoreWarnings();
938 - if ( !$ok ) { // not present or not empty
939 - $status->fatal( 'backend-fail-opentemp', $tmpPath );
940 - return $status;
941 - }
942 -
943 - // Build up the temp file using the source chunks (in order)...
944 - $tmpHandle = fopen( $tmpPath, 'ab' );
945 - if ( $tmpHandle === false ) {
946 - $status->fatal( 'backend-fail-opentemp', $tmpPath );
947 - return $status;
948 - }
949 - foreach ( $params['srcs'] as $virtualSource ) {
950 - // Get a local FS version of the chunk
951 - $tmpFile = $this->getLocalReference( array( 'src' => $virtualSource ) );
952 - if ( !$tmpFile ) {
953 - $status->fatal( 'backend-fail-read', $virtualSource );
954 - return $status;
955 - }
956 - // Get a handle to the local FS version
957 - $sourceHandle = fopen( $tmpFile->getPath(), 'r' );
958 - if ( $sourceHandle === false ) {
959 - fclose( $tmpHandle );
960 - $status->fatal( 'backend-fail-read', $virtualSource );
961 - return $status;
962 - }
963 - // Append chunk to file (pass chunk size to avoid magic quotes)
964 - if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
965 - fclose( $sourceHandle );
966 - fclose( $tmpHandle );
967 - $status->fatal( 'backend-fail-writetemp', $tmpPath );
968 - return $status;
969 - }
970 - fclose( $sourceHandle );
971 - }
972 - if ( !fclose( $tmpHandle ) ) {
973 - $status->fatal( 'backend-fail-closetemp', $tmpPath );
974 - return $status;
975 - }
976 -
977 - clearstatcache(); // temp file changed
978 -
979 - return $status;
980 - }
981 -
982 - /**
983 - * @see FileBackend::doPrepare()
984 - * @return Status
985 - */
986 - final protected function doPrepare( array $params ) {
987 - wfProfileIn( __METHOD__ );
988 -
989 - $status = Status::newGood();
990 - list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
991 - if ( $dir === null ) {
992 - $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
993 - wfProfileOut( __METHOD__ );
994 - return $status; // invalid storage path
995 - }
996 -
997 - if ( $shard !== null ) { // confined to a single container/shard
998 - $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
999 - } else { // directory is on several shards
1000 - wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
1001 - list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
1002 - foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
1003 - $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
1004 - }
1005 - }
1006 -
1007 - wfProfileOut( __METHOD__ );
1008 - return $status;
1009 - }
1010 -
1011 - /**
1012 - * @see FileBackendStore::doPrepare()
1013 - * @return Status
1014 - */
1015 - protected function doPrepareInternal( $container, $dir, array $params ) {
1016 - return Status::newGood();
1017 - }
1018 -
1019 - /**
1020 - * @see FileBackend::doSecure()
1021 - * @return Status
1022 - */
1023 - final protected function doSecure( array $params ) {
1024 - wfProfileIn( __METHOD__ );
1025 - $status = Status::newGood();
1026 -
1027 - list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
1028 - if ( $dir === null ) {
1029 - $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
1030 - wfProfileOut( __METHOD__ );
1031 - return $status; // invalid storage path
1032 - }
1033 -
1034 - if ( $shard !== null ) { // confined to a single container/shard
1035 - $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
1036 - } else { // directory is on several shards
1037 - wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
1038 - list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
1039 - foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
1040 - $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
1041 - }
1042 - }
1043 -
1044 - wfProfileOut( __METHOD__ );
1045 - return $status;
1046 - }
1047 -
1048 - /**
1049 - * @see FileBackendStore::doSecure()
1050 - * @return Status
1051 - */
1052 - protected function doSecureInternal( $container, $dir, array $params ) {
1053 - return Status::newGood();
1054 - }
1055 -
1056 - /**
1057 - * @see FileBackend::doClean()
1058 - * @return Status
1059 - */
1060 - final protected function doClean( array $params ) {
1061 - wfProfileIn( __METHOD__ );
1062 - $status = Status::newGood();
1063 -
1064 - list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
1065 - if ( $dir === null ) {
1066 - $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
1067 - wfProfileOut( __METHOD__ );
1068 - return $status; // invalid storage path
1069 - }
1070 -
1071 - // Attempt to lock this directory...
1072 - $filesLockEx = array( $params['dir'] );
1073 - $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
1074 - if ( !$status->isOK() ) {
1075 - wfProfileOut( __METHOD__ );
1076 - return $status; // abort
1077 - }
1078 -
1079 - if ( $shard !== null ) { // confined to a single container/shard
1080 - $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
1081 - } else { // directory is on several shards
1082 - wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
1083 - list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
1084 - foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
1085 - $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
1086 - }
1087 - }
1088 -
1089 - wfProfileOut( __METHOD__ );
1090 - return $status;
1091 - }
1092 -
1093 - /**
1094 - * @see FileBackendStore::doClean()
1095 - * @return Status
1096 - */
1097 - protected function doCleanInternal( $container, $dir, array $params ) {
1098 - return Status::newGood();
1099 - }
1100 -
1101 - /**
1102 - * @see FileBackend::fileExists()
1103 - * @return bool|null
1104 - */
1105 - final public function fileExists( array $params ) {
1106 - wfProfileIn( __METHOD__ );
1107 - $stat = $this->getFileStat( $params );
1108 - wfProfileOut( __METHOD__ );
1109 - return ( $stat === null ) ? null : (bool)$stat; // null => failure
1110 - }
1111 -
1112 - /**
1113 - * @see FileBackend::getFileTimestamp()
1114 - * @return bool
1115 - */
1116 - final public function getFileTimestamp( array $params ) {
1117 - wfProfileIn( __METHOD__ );
1118 - $stat = $this->getFileStat( $params );
1119 - wfProfileOut( __METHOD__ );
1120 - return $stat ? $stat['mtime'] : false;
1121 - }
1122 -
1123 - /**
1124 - * @see FileBackend::getFileSize()
1125 - * @return bool
1126 - */
1127 - final public function getFileSize( array $params ) {
1128 - wfProfileIn( __METHOD__ );
1129 - $stat = $this->getFileStat( $params );
1130 - wfProfileOut( __METHOD__ );
1131 - return $stat ? $stat['size'] : false;
1132 - }
1133 -
1134 - /**
1135 - * @see FileBackend::getFileStat()
1136 - * @return bool
1137 - */
1138 - final public function getFileStat( array $params ) {
1139 - wfProfileIn( __METHOD__ );
1140 - $path = self::normalizeStoragePath( $params['src'] );
1141 - if ( $path === null ) {
1142 - wfProfileOut( __METHOD__ );
1143 - return false; // invalid storage path
1144 - }
1145 - $latest = !empty( $params['latest'] );
1146 - if ( isset( $this->cache[$path]['stat'] ) ) {
1147 - // If we want the latest data, check that this cached
1148 - // value was in fact fetched with the latest available data.
1149 - if ( !$latest || $this->cache[$path]['stat']['latest'] ) {
1150 - wfProfileOut( __METHOD__ );
1151 - return $this->cache[$path]['stat'];
1152 - }
1153 - }
1154 - wfProfileIn( __METHOD__ . '-miss' );
1155 - $stat = $this->doGetFileStat( $params );
1156 - wfProfileOut( __METHOD__ . '-miss' );
1157 - if ( is_array( $stat ) ) { // don't cache negatives
1158 - $this->trimCache(); // limit memory
1159 - $this->cache[$path]['stat'] = $stat;
1160 - $this->cache[$path]['stat']['latest'] = $latest;
1161 - }
1162 - wfProfileOut( __METHOD__ );
1163 - return $stat;
1164 - }
1165 -
1166 - /**
1167 - * @see FileBackendStore::getFileStat()
1168 - */
1169 - abstract protected function doGetFileStat( array $params );
1170 -
1171 - /**
1172 - * @see FileBackend::getFileContents()
1173 - * @return bool|string
1174 - */
1175 - public function getFileContents( array $params ) {
1176 - wfProfileIn( __METHOD__ );
1177 - $tmpFile = $this->getLocalReference( $params );
1178 - if ( !$tmpFile ) {
1179 - wfProfileOut( __METHOD__ );
1180 - return false;
1181 - }
1182 - wfSuppressWarnings();
1183 - $data = file_get_contents( $tmpFile->getPath() );
1184 - wfRestoreWarnings();
1185 - wfProfileOut( __METHOD__ );
1186 - return $data;
1187 - }
1188 -
1189 - /**
1190 - * @see FileBackend::getFileSha1Base36()
1191 - * @return bool|string
1192 - */
1193 - final public function getFileSha1Base36( array $params ) {
1194 - wfProfileIn( __METHOD__ );
1195 - $path = $params['src'];
1196 - if ( isset( $this->cache[$path]['sha1'] ) ) {
1197 - wfProfileOut( __METHOD__ );
1198 - return $this->cache[$path]['sha1'];
1199 - }
1200 - wfProfileIn( __METHOD__ . '-miss' );
1201 - $hash = $this->doGetFileSha1Base36( $params );
1202 - wfProfileOut( __METHOD__ . '-miss' );
1203 - if ( $hash ) { // don't cache negatives
1204 - $this->trimCache(); // limit memory
1205 - $this->cache[$path]['sha1'] = $hash;
1206 - }
1207 - wfProfileOut( __METHOD__ );
1208 - return $hash;
1209 - }
1210 -
1211 - /**
1212 - * @see FileBackendStore::getFileSha1Base36()
1213 - * @return bool
1214 - */
1215 - protected function doGetFileSha1Base36( array $params ) {
1216 - $fsFile = $this->getLocalReference( $params );
1217 - if ( !$fsFile ) {
1218 - return false;
1219 - } else {
1220 - return $fsFile->getSha1Base36();
1221 - }
1222 - }
1223 -
1224 - /**
1225 - * @see FileBackend::getFileProps()
1226 - * @return Array
1227 - */
1228 - final public function getFileProps( array $params ) {
1229 - wfProfileIn( __METHOD__ );
1230 - $fsFile = $this->getLocalReference( $params );
1231 - $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
1232 - wfProfileOut( __METHOD__ );
1233 - return $props;
1234 - }
1235 -
1236 - /**
1237 - * @see FileBackend::getLocalReference()
1238 - * @return TempFSFile|null
1239 - */
1240 - public function getLocalReference( array $params ) {
1241 - wfProfileIn( __METHOD__ );
1242 - $path = $params['src'];
1243 - if ( isset( $this->expensiveCache[$path]['localRef'] ) ) {
1244 - wfProfileOut( __METHOD__ );
1245 - return $this->expensiveCache[$path]['localRef'];
1246 - }
1247 - $tmpFile = $this->getLocalCopy( $params );
1248 - if ( $tmpFile ) { // don't cache negatives
1249 - $this->trimExpensiveCache(); // limit memory
1250 - $this->expensiveCache[$path]['localRef'] = $tmpFile;
1251 - }
1252 - wfProfileOut( __METHOD__ );
1253 - return $tmpFile;
1254 - }
1255 -
1256 - /**
1257 - * @see FileBackend::streamFile()
1258 - * @return Status
1259 - */
1260 - final public function streamFile( array $params ) {
1261 - wfProfileIn( __METHOD__ );
1262 - $status = Status::newGood();
1263 -
1264 - $info = $this->getFileStat( $params );
1265 - if ( !$info ) { // let StreamFile handle the 404
1266 - $status->fatal( 'backend-fail-notexists', $params['src'] );
1267 - }
1268 -
1269 - // Set output buffer and HTTP headers for stream
1270 - $extraHeaders = isset( $params['headers'] ) ? $params['headers'] : array();
1271 - $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
1272 - if ( $res == StreamFile::NOT_MODIFIED ) {
1273 - // do nothing; client cache is up to date
1274 - } elseif ( $res == StreamFile::READY_STREAM ) {
1275 - wfProfileIn( __METHOD__ . '-send' );
1276 - $status = $this->doStreamFile( $params );
1277 - wfProfileOut( __METHOD__ . '-send' );
1278 - } else {
1279 - $status->fatal( 'backend-fail-stream', $params['src'] );
1280 - }
1281 -
1282 - wfProfileOut( __METHOD__ );
1283 - return $status;
1284 - }
1285 -
1286 - /**
1287 - * @see FileBackendStore::streamFile()
1288 - * @return Status
1289 - */
1290 - protected function doStreamFile( array $params ) {
1291 - $status = Status::newGood();
1292 -
1293 - $fsFile = $this->getLocalReference( $params );
1294 - if ( !$fsFile ) {
1295 - $status->fatal( 'backend-fail-stream', $params['src'] );
1296 - } elseif ( !readfile( $fsFile->getPath() ) ) {
1297 - $status->fatal( 'backend-fail-stream', $params['src'] );
1298 - }
1299 -
1300 - return $status;
1301 - }
1302 -
1303 - /**
1304 - * @copydoc FileBackend::getFileList()
1305 - * @return Array|null|Traversable
1306 - */
1307 - final public function getFileList( array $params ) {
1308 - list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
1309 - if ( $dir === null ) { // invalid storage path
1310 - return null;
1311 - }
1312 - if ( $shard !== null ) {
1313 - // File listing is confined to a single container/shard
1314 - return $this->getFileListInternal( $fullCont, $dir, $params );
1315 - } else {
1316 - wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
1317 - // File listing spans multiple containers/shards
1318 - list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
1319 - return new FileBackendStoreShardListIterator( $this,
1320 - $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
1321 - }
1322 - }
1323 -
1324 - /**
1325 - * Do not call this function from places outside FileBackend
1326 - *
1327 - * @see FileBackendStore::getFileList()
1328 - *
1329 - * @param $container string Resolved container name
1330 - * @param $dir string Resolved path relative to container
1331 - * @param $params Array
1332 - * @return Traversable|Array|null
1333 - */
1334 - abstract public function getFileListInternal( $container, $dir, array $params );
1335 -
1336 - /**
1337 - * Get the list of supported operations and their corresponding FileOp classes.
1338 - *
1339 - * @return Array
1340 - */
1341 - protected function supportedOperations() {
1342 - return array(
1343 - 'store' => 'StoreFileOp',
1344 - 'copy' => 'CopyFileOp',
1345 - 'move' => 'MoveFileOp',
1346 - 'delete' => 'DeleteFileOp',
1347 - 'create' => 'CreateFileOp',
1348 - 'null' => 'NullFileOp'
1349 - );
1350 - }
1351 -
1352 - /**
1353 - * Return a list of FileOp objects from a list of operations.
1354 - * Do not call this function from places outside FileBackend.
1355 - *
1356 - * The result must have the same number of items as the input.
1357 - * An exception is thrown if an unsupported operation is requested.
1358 - *
1359 - * @param $ops Array Same format as doOperations()
1360 - * @return Array List of FileOp objects
1361 - * @throws MWException
1362 - */
1363 - final public function getOperations( array $ops ) {
1364 - $supportedOps = $this->supportedOperations();
1365 -
1366 - $performOps = array(); // array of FileOp objects
1367 - // Build up ordered array of FileOps...
1368 - foreach ( $ops as $operation ) {
1369 - $opName = $operation['op'];
1370 - if ( isset( $supportedOps[$opName] ) ) {
1371 - $class = $supportedOps[$opName];
1372 - // Get params for this operation
1373 - $params = $operation;
1374 - // Append the FileOp class
1375 - $performOps[] = new $class( $this, $params );
1376 - } else {
1377 - throw new MWException( "Operation `$opName` is not supported." );
1378 - }
1379 - }
1380 -
1381 - return $performOps;
1382 - }
1383 -
1384 - /**
1385 - * @see FileBackend::doOperationsInternal()
1386 - * @return Status
1387 - */
1388 - protected function doOperationsInternal( array $ops, array $opts ) {
1389 - wfProfileIn( __METHOD__ );
1390 - $status = Status::newGood();
1391 -
1392 - // Build up a list of FileOps...
1393 - $performOps = $this->getOperations( $ops );
1394 -
1395 - // Acquire any locks as needed...
1396 - if ( empty( $opts['nonLocking'] ) ) {
1397 - // Build up a list of files to lock...
1398 - $filesLockEx = $filesLockSh = array();
1399 - foreach ( $performOps as $fileOp ) {
1400 - $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() );
1401 - $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() );
1402 - }
1403 - // Optimization: if doing an EX lock anyway, don't also set an SH one
1404 - $filesLockSh = array_diff( $filesLockSh, $filesLockEx );
1405 - // Get a shared lock on the parent directory of each path changed
1406 - $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
1407 - // Try to lock those files for the scope of this function...
1408 - $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
1409 - $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
1410 - if ( !$status->isOK() ) {
1411 - wfProfileOut( __METHOD__ );
1412 - return $status; // abort
1413 - }
1414 - }
1415 -
1416 - // Clear any cache entries (after locks acquired)
1417 - $this->clearCache();
1418 -
1419 - // Actually attempt the operation batch...
1420 - $subStatus = FileOp::attemptBatch( $performOps, $opts );
1421 -
1422 - // Merge errors into status fields
1423 - $status->merge( $subStatus );
1424 - $status->success = $subStatus->success; // not done in merge()
1425 -
1426 - wfProfileOut( __METHOD__ );
1427 - return $status;
1428 - }
1429 -
1430 - /**
1431 - * @see FileBackend::clearCache()
1432 - */
1433 - final public function clearCache( array $paths = null ) {
1434 - if ( is_array( $paths ) ) {
1435 - $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
1436 - $paths = array_filter( $paths, 'strlen' ); // remove nulls
1437 - }
1438 - if ( $paths === null ) {
1439 - $this->cache = array();
1440 - $this->expensiveCache = array();
1441 - } else {
1442 - foreach ( $paths as $path ) {
1443 - unset( $this->cache[$path] );
1444 - unset( $this->expensiveCache[$path] );
1445 - }
1446 - }
1447 - $this->doClearCache( $paths );
1448 - }
1449 -
1450 - /**
1451 - * Clears any additional stat caches for storage paths
1452 - *
1453 - * @see FileBackend::clearCache()
1454 - *
1455 - * @param $paths Array Storage paths (optional)
1456 - * @return void
1457 - */
1458 - protected function doClearCache( array $paths = null ) {}
1459 -
1460 - /**
1461 - * Prune the inexpensive cache if it is too big to add an item
1462 - *
1463 - * @return void
1464 - */
1465 - protected function trimCache() {
1466 - if ( count( $this->cache ) >= $this->maxCacheSize ) {
1467 - reset( $this->cache );
1468 - unset( $this->cache[key( $this->cache )] );
1469 - }
1470 - }
1471 -
1472 - /**
1473 - * Prune the expensive cache if it is too big to add an item
1474 - *
1475 - * @return void
1476 - */
1477 - protected function trimExpensiveCache() {
1478 - if ( count( $this->expensiveCache ) >= $this->maxExpensiveCacheSize ) {
1479 - reset( $this->expensiveCache );
1480 - unset( $this->expensiveCache[key( $this->expensiveCache )] );
1481 - }
1482 - }
1483 -
1484 - /**
1485 - * Check if a container name is valid.
1486 - * This checks for for length and illegal characters.
1487 - *
1488 - * @param $container string
1489 - * @return bool
1490 - */
1491 - final protected static function isValidContainerName( $container ) {
1492 - // This accounts for Swift and S3 restrictions while leaving room
1493 - // for things like '.xxx' (hex shard chars) or '.seg' (segments).
1494 - // This disallows directory separators or traversal characters.
1495 - // Note that matching strings URL encode to the same string;
1496 - // in Swift, the length restriction is *after* URL encoding.
1497 - return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container );
1498 - }
1499 -
1500 - /**
1501 - * Splits a storage path into an internal container name,
1502 - * an internal relative file name, and a container shard suffix.
1503 - * Any shard suffix is already appended to the internal container name.
1504 - * This also checks that the storage path is valid and within this backend.
1505 - *
1506 - * If the container is sharded but a suffix could not be determined,
1507 - * this means that the path can only refer to a directory and can only
1508 - * be scanned by looking in all the container shards.
1509 - *
1510 - * @param $storagePath string
1511 - * @return Array (container, path, container suffix) or (null, null, null) if invalid
1512 - */
1513 - final protected function resolveStoragePath( $storagePath ) {
1514 - list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
1515 - if ( $backend === $this->name ) { // must be for this backend
1516 - $relPath = self::normalizeContainerPath( $relPath );
1517 - if ( $relPath !== null ) {
1518 - // Get shard for the normalized path if this container is sharded
1519 - $cShard = $this->getContainerShard( $container, $relPath );
1520 - // Validate and sanitize the relative path (backend-specific)
1521 - $relPath = $this->resolveContainerPath( $container, $relPath );
1522 - if ( $relPath !== null ) {
1523 - // Prepend any wiki ID prefix to the container name
1524 - $container = $this->fullContainerName( $container );
1525 - if ( self::isValidContainerName( $container ) ) {
1526 - // Validate and sanitize the container name (backend-specific)
1527 - $container = $this->resolveContainerName( "{$container}{$cShard}" );
1528 - if ( $container !== null ) {
1529 - return array( $container, $relPath, $cShard );
1530 - }
1531 - }
1532 - }
1533 - }
1534 - }
1535 - return array( null, null, null );
1536 - }
1537 -
1538 - /**
1539 - * Like resolveStoragePath() except null values are returned if
1540 - * the container is sharded and the shard could not be determined.
1541 - *
1542 - * @see FileBackendStore::resolveStoragePath()
1543 - *
1544 - * @param $storagePath string
1545 - * @return Array (container, path) or (null, null) if invalid
1546 - */
1547 - final protected function resolveStoragePathReal( $storagePath ) {
1548 - list( $container, $relPath, $cShard ) = $this->resolveStoragePath( $storagePath );
1549 - if ( $cShard !== null ) {
1550 - return array( $container, $relPath );
1551 - }
1552 - return array( null, null );
1553 - }
1554 -
1555 - /**
1556 - * Get the container name shard suffix for a given path.
1557 - * Any empty suffix means the container is not sharded.
1558 - *
1559 - * @param $container string Container name
1560 - * @param $relStoragePath string Storage path relative to the container
1561 - * @return string|null Returns null if shard could not be determined
1562 - */
1563 - final protected function getContainerShard( $container, $relPath ) {
1564 - list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
1565 - if ( $levels == 1 || $levels == 2 ) {
1566 - // Hash characters are either base 16 or 36
1567 - $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
1568 - // Get a regex that represents the shard portion of paths.
1569 - // The concatenation of the captures gives us the shard.
1570 - if ( $levels === 1 ) { // 16 or 36 shards per container
1571 - $hashDirRegex = '(' . $char . ')';
1572 - } else { // 256 or 1296 shards per container
1573 - if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
1574 - $hashDirRegex = $char . '/(' . $char . '{2})';
1575 - } else { // short hash dir format (e.g. "a/b/c")
1576 - $hashDirRegex = '(' . $char . ')/(' . $char . ')';
1577 - }
1578 - }
1579 - // Allow certain directories to be above the hash dirs so as
1580 - // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
1581 - // They must be 2+ chars to avoid any hash directory ambiguity.
1582 - $m = array();
1583 - if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
1584 - return '.' . implode( '', array_slice( $m, 1 ) );
1585 - }
1586 - return null; // failed to match
1587 - }
1588 - return ''; // no sharding
1589 - }
1590 -
1591 - /**
1592 - * Get the sharding config for a container.
1593 - * If greater than 0, then all file storage paths within
1594 - * the container are required to be hashed accordingly.
1595 - *
1596 - * @param $container string
1597 - * @return Array (integer levels, integer base, repeat flag) or (0, 0, false)
1598 - */
1599 - final protected function getContainerHashLevels( $container ) {
1600 - if ( isset( $this->shardViaHashLevels[$container] ) ) {
1601 - $config = $this->shardViaHashLevels[$container];
1602 - $hashLevels = (int)$config['levels'];
1603 - if ( $hashLevels == 1 || $hashLevels == 2 ) {
1604 - $hashBase = (int)$config['base'];
1605 - if ( $hashBase == 16 || $hashBase == 36 ) {
1606 - return array( $hashLevels, $hashBase, $config['repeat'] );
1607 - }
1608 - }
1609 - }
1610 - return array( 0, 0, false ); // no sharding
1611 - }
1612 -
1613 - /**
1614 - * Get a list of full container shard suffixes for a container
1615 - *
1616 - * @param $container string
1617 - * @return Array
1618 - */
1619 - final protected function getContainerSuffixes( $container ) {
1620 - $shards = array();
1621 - list( $digits, $base ) = $this->getContainerHashLevels( $container );
1622 - if ( $digits > 0 ) {
1623 - $numShards = pow( $base, $digits );
1624 - for ( $index = 0; $index < $numShards; $index++ ) {
1625 - $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits );
1626 - }
1627 - }
1628 - return $shards;
1629 - }
1630 -
1631 - /**
1632 - * Get the full container name, including the wiki ID prefix
1633 - *
1634 - * @param $container string
1635 - * @return string
1636 - */
1637 - final protected function fullContainerName( $container ) {
1638 - if ( $this->wikiId != '' ) {
1639 - return "{$this->wikiId}-$container";
1640 - } else {
1641 - return $container;
1642 - }
1643 - }
1644 -
1645 - /**
1646 - * Resolve a container name, checking if it's allowed by the backend.
1647 - * This is intended for internal use, such as encoding illegal chars.
1648 - * Subclasses can override this to be more restrictive.
1649 - *
1650 - * @param $container string
1651 - * @return string|null
1652 - */
1653 - protected function resolveContainerName( $container ) {
1654 - return $container;
1655 - }
1656 -
1657 - /**
1658 - * Resolve a relative storage path, checking if it's allowed by the backend.
1659 - * This is intended for internal use, such as encoding illegal chars or perhaps
1660 - * getting absolute paths (e.g. FS based backends). Note that the relative path
1661 - * may be the empty string (e.g. the path is simply to the container).
1662 - *
1663 - * @param $container string Container name
1664 - * @param $relStoragePath string Storage path relative to the container
1665 - * @return string|null Path or null if not valid
1666 - */
1667 - protected function resolveContainerPath( $container, $relStoragePath ) {
1668 - return $relStoragePath;
1669 - }
1670 -}
1671 -
1672 -/**
1673 - * FileBackendStore helper function to handle file listings that span container shards.
1674 - * Do not use this class from places outside of FileBackendStore.
1675 - *
1676 - * @ingroup FileBackend
1677 - */
1678 -class FileBackendStoreShardListIterator implements Iterator {
1679 - /* @var FileBackendStore */
1680 - protected $backend;
1681 - /* @var Array */
1682 - protected $params;
1683 - /* @var Array */
1684 - protected $shardSuffixes;
1685 - protected $container; // string
1686 - protected $directory; // string
1687 -
1688 - /* @var Traversable */
1689 - protected $iter;
1690 - protected $curShard = 0; // integer
1691 - protected $pos = 0; // integer
1692 -
1693 - /**
1694 - * @param $backend FileBackendStore
1695 - * @param $container string Full storage container name
1696 - * @param $dir string Storage directory relative to container
1697 - * @param $suffixes Array List of container shard suffixes
1698 - * @param $params Array
1699 - */
1700 - public function __construct(
1701 - FileBackendStore $backend, $container, $dir, array $suffixes, array $params
1702 - ) {
1703 - $this->backend = $backend;
1704 - $this->container = $container;
1705 - $this->directory = $dir;
1706 - $this->shardSuffixes = $suffixes;
1707 - $this->params = $params;
1708 - }
1709 -
1710 - public function current() {
1711 - if ( is_array( $this->iter ) ) {
1712 - return current( $this->iter );
1713 - } else {
1714 - return $this->iter->current();
1715 - }
1716 - }
1717 -
1718 - public function key() {
1719 - return $this->pos;
1720 - }
1721 -
1722 - public function next() {
1723 - ++$this->pos;
1724 - if ( is_array( $this->iter ) ) {
1725 - next( $this->iter );
1726 - } else {
1727 - $this->iter->next();
1728 - }
1729 - // Find the next non-empty shard if no elements are left
1730 - $this->nextShardIteratorIfNotValid();
1731 - }
1732 -
1733 - /**
1734 - * If the iterator for this container shard is out of items,
1735 - * then move on to the next container that has items.
1736 - * If there are none, then it advances to the last container.
1737 - */
1738 - protected function nextShardIteratorIfNotValid() {
1739 - while ( !$this->valid() ) {
1740 - if ( ++$this->curShard >= count( $this->shardSuffixes ) ) {
1741 - break; // no more container shards
1742 - }
1743 - $this->setIteratorFromCurrentShard();
1744 - }
1745 - }
1746 -
1747 - protected function setIteratorFromCurrentShard() {
1748 - $suffix = $this->shardSuffixes[$this->curShard];
1749 - $this->iter = $this->backend->getFileListInternal(
1750 - "{$this->container}{$suffix}", $this->directory, $this->params );
1751 - }
1752 -
1753 - public function rewind() {
1754 - $this->pos = 0;
1755 - $this->curShard = 0;
1756 - $this->setIteratorFromCurrentShard();
1757 - // Find the next non-empty shard if this one has no elements
1758 - $this->nextShardIteratorIfNotValid();
1759 - }
1760 -
1761 - public function valid() {
1762 - if ( $this->iter == null ) {
1763 - return false; // some failure?
1764 - } elseif ( is_array( $this->iter ) ) {
1765 - return ( current( $this->iter ) !== false ); // no paths can have this value
1766 - } else {
1767 - return $this->iter->valid();
1768 - }
1769 - }
1770 -}
Index: trunk/phase3/includes/AutoLoader.php
@@ -500,9 +500,9 @@
501501 # includes/filerepo/backend
502502 'FileBackendGroup' => 'includes/filerepo/backend/FileBackendGroup.php',
503503 'FileBackend' => 'includes/filerepo/backend/FileBackend.php',
504 - 'FileBackendStore' => 'includes/filerepo/backend/FileBackend.php',
 504+ 'FileBackendStore' => 'includes/filerepo/backend/FileBackendStore.php',
 505+ 'FileBackendStoreShardListIterator' => 'includes/filerepo/backend/FileBackendStore.php',
505506 'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php',
506 - 'FileBackendStoreShardListIterator' => 'includes/filerepo/backend/FileBackend.php',
507507 'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php',
508508 'FSFileBackendFileList' => 'includes/filerepo/backend/FSFileBackend.php',
509509 'SwiftFileBackend' => 'includes/filerepo/backend/SwiftFileBackend.php',

Status & tagging log