Merge "[FileBackend] Some documentation and minor cleanups."
[lhc/web/wiklou.git] / includes / filerepo / backend / FileBackendStore.php
index e96f257..b87a69f 100644 (file)
  *
  * This class defines the methods as abstract that subclasses must implement.
  * Outside callers should *not* use functions with "Internal" in the name.
- * 
+ *
  * The FileBackend operations are implemented using basic functions
  * such as storeInternal(), copyInternal(), deleteInternal() and the like.
  * This class is also responsible for path resolution and sanitization.
- * 
+ *
  * @ingroup FileBackend
  * @since 1.19
  */
 abstract class FileBackendStore extends FileBackend {
+       /** @var BagOStuff */
+       protected $memCache;
+
        /** @var Array Map of paths to small (RAM/disk) cache items */
        protected $cache = array(); // (storage path => key => value)
-       protected $maxCacheSize = 100; // integer; max paths with entries
+       protected $maxCacheSize = 300; // integer; max paths with entries
        /** @var Array Map of paths to large (RAM/disk) cache items */
        protected $expensiveCache = array(); // (storage path => key => value)
-       protected $maxExpensiveCacheSize = 10; // integer; max paths with entries
+       protected $maxExpensiveCacheSize = 5; // integer; max paths with entries
 
        /** @var Array Map of container names to sharding settings */
        protected $shardViaHashLevels = array(); // (container name => config array)
 
        protected $maxFileSize = 4294967296; // integer bytes (4GiB)
 
+       /**
+        * @see FileBackend::__construct()
+        *
+        * @param $config Array
+        */
+       public function __construct( array $config ) {
+               parent::__construct( $config );
+               $this->memCache = new EmptyBagOStuff(); // disabled by default
+       }
+
        /**
         * Get the maximum allowable file size given backend
         * medium restrictions and basic performance constraints.
         * Do not call this function from places outside FileBackend and FileOp.
-        * 
-        * @return integer Bytes 
+        *
+        * @return integer Bytes
         */
        final public function maxFileSizeInternal() {
                return $this->maxFileSize;
@@ -55,17 +68,18 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * Create a file in the backend with the given contents.
         * Do not call this function from places outside FileBackend and FileOp.
-        * 
+        *
         * $params include:
         *     content       : the raw file contents
         *     dst           : destination storage path
         *     overwrite     : overwrite any file that exists at the destination
-        * 
+        *
         * @param $params Array
         * @return Status
         */
        final public function createInternal( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
                        $status = Status::newFatal( 'backend-fail-maxsize',
                                $params['dst'], $this->maxFileSizeInternal() );
@@ -73,6 +87,7 @@ abstract class FileBackendStore extends FileBackend {
                        $status = $this->doCreateInternal( $params );
                        $this->clearCache( array( $params['dst'] ) );
                }
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -85,23 +100,25 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * Store a file into the backend from a file on disk.
         * Do not call this function from places outside FileBackend and FileOp.
-        * 
+        *
         * $params include:
         *     src           : source path on disk
         *     dst           : destination storage path
         *     overwrite     : overwrite any file that exists at the destination
-        * 
+        *
         * @param $params Array
         * @return Status
         */
        final public function storeInternal( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                if ( filesize( $params['src'] ) > $this->maxFileSizeInternal() ) {
                        $status = Status::newFatal( 'backend-fail-store', $params['dst'] );
                } else {
                        $status = $this->doStoreInternal( $params );
                        $this->clearCache( array( $params['dst'] ) );
                }
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -114,19 +131,21 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * Copy a file from one storage path to another in the backend.
         * Do not call this function from places outside FileBackend and FileOp.
-        * 
+        *
         * $params include:
         *     src           : source storage path
         *     dst           : destination storage path
         *     overwrite     : overwrite any file that exists at the destination
-        * 
+        *
         * @param $params Array
         * @return Status
         */
        final public function copyInternal( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = $this->doCopyInternal( $params );
                $this->clearCache( array( $params['dst'] ) );
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -139,18 +158,20 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * Delete a file at the storage path.
         * Do not call this function from places outside FileBackend and FileOp.
-        * 
+        *
         * $params include:
         *     src                 : source storage path
         *     ignoreMissingSource : do nothing if the source file does not exist
-        * 
+        *
         * @param $params Array
         * @return Status
         */
        final public function deleteInternal( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = $this->doDeleteInternal( $params );
                $this->clearCache( array( $params['src'] ) );
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -163,19 +184,21 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * Move a file from one storage path to another in the backend.
         * Do not call this function from places outside FileBackend and FileOp.
-        * 
+        *
         * $params include:
         *     src           : source storage path
         *     dst           : destination storage path
         *     overwrite     : overwrite any file that exists at the destination
-        * 
+        *
         * @param $params Array
         * @return Status
         */
        final public function moveInternal( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = $this->doMoveInternal( $params );
                $this->clearCache( array( $params['src'], $params['dst'] ) );
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -201,6 +224,7 @@ abstract class FileBackendStore extends FileBackend {
         */
        final public function concatenate( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = Status::newGood();
 
                // Try to lock the source files for the scope of this function
@@ -210,6 +234,7 @@ abstract class FileBackendStore extends FileBackend {
                        $status->merge( $this->doConcatenate( $params ) );
                }
 
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -276,11 +301,13 @@ abstract class FileBackendStore extends FileBackend {
         */
        final protected function doPrepare( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
 
                $status = Status::newGood();
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
+                       wfProfileOut( __METHOD__ . '-' . $this->name );
                        wfProfileOut( __METHOD__ );
                        return $status; // invalid storage path
                }
@@ -295,6 +322,7 @@ abstract class FileBackendStore extends FileBackend {
                        }
                }
 
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -313,11 +341,13 @@ abstract class FileBackendStore extends FileBackend {
         */
        final protected function doSecure( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = Status::newGood();
 
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
+                       wfProfileOut( __METHOD__ . '-' . $this->name );
                        wfProfileOut( __METHOD__ );
                        return $status; // invalid storage path
                }
@@ -332,6 +362,7 @@ abstract class FileBackendStore extends FileBackend {
                        }
                }
 
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -350,11 +381,24 @@ abstract class FileBackendStore extends FileBackend {
         */
        final protected function doClean( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = Status::newGood();
 
+               // Recursive: first delete all empty subdirs recursively
+               if ( !empty( $params['recursive'] ) && !$this->directoriesAreVirtual() ) {
+                       $subDirsRel = $this->getTopDirectoryList( array( 'dir' => $params['dir'] ) );
+                       if ( $subDirsRel !== null ) { // no errors
+                               foreach ( $subDirsRel as $subDirRel ) {
+                                       $subDir = $params['dir'] . "/{$subDirRel}"; // full path
+                                       $status->merge( $this->doClean( array( 'dir' => $subDir ) + $params ) );
+                               }
+                       }
+               }
+
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
+                       wfProfileOut( __METHOD__ . '-' . $this->name );
                        wfProfileOut( __METHOD__ );
                        return $status; // invalid storage path
                }
@@ -363,20 +407,24 @@ abstract class FileBackendStore extends FileBackend {
                $filesLockEx = array( $params['dir'] );
                $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
                if ( !$status->isOK() ) {
+                       wfProfileOut( __METHOD__ . '-' . $this->name );
                        wfProfileOut( __METHOD__ );
                        return $status; // abort
                }
 
                if ( $shard !== null ) { // confined to a single container/shard
                        $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
+                       $this->deleteContainerCache( $fullCont ); // purge cache
                } else { // directory is on several shards
                        wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
                        list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
                        foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
                                $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
+                               $this->deleteContainerCache( "{$fullCont}{$suffix}" ); // purge cache
                        }
                }
 
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -395,7 +443,9 @@ abstract class FileBackendStore extends FileBackend {
         */
        final public function fileExists( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $stat = $this->getFileStat( $params );
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return ( $stat === null ) ? null : (bool)$stat; // null => failure
        }
@@ -406,7 +456,9 @@ abstract class FileBackendStore extends FileBackend {
         */
        final public function getFileTimestamp( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $stat = $this->getFileStat( $params );
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $stat ? $stat['mtime'] : false;
        }
@@ -417,7 +469,9 @@ abstract class FileBackendStore extends FileBackend {
         */
        final public function getFileSize( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $stat = $this->getFileStat( $params );
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $stat ? $stat['size'] : false;
        }
@@ -428,8 +482,10 @@ abstract class FileBackendStore extends FileBackend {
         */
        final public function getFileStat( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $path = self::normalizeStoragePath( $params['src'] );
                if ( $path === null ) {
+                       wfProfileOut( __METHOD__ . '-' . $this->name );
                        wfProfileOut( __METHOD__ );
                        return false; // invalid storage path
                }
@@ -438,18 +494,23 @@ abstract class FileBackendStore extends FileBackend {
                        // If we want the latest data, check that this cached
                        // value was in fact fetched with the latest available data.
                        if ( !$latest || $this->cache[$path]['stat']['latest'] ) {
+                               $this->pingCache( $path ); // LRU
+                               wfProfileOut( __METHOD__ . '-' . $this->name );
                                wfProfileOut( __METHOD__ );
                                return $this->cache[$path]['stat'];
                        }
                }
                wfProfileIn( __METHOD__ . '-miss' );
+               wfProfileIn( __METHOD__ . '-miss-' . $this->name );
                $stat = $this->doGetFileStat( $params );
+               wfProfileOut( __METHOD__ . '-miss-' . $this->name );
                wfProfileOut( __METHOD__ . '-miss' );
                if ( is_array( $stat ) ) { // don't cache negatives
                        $this->trimCache(); // limit memory
                        $this->cache[$path]['stat'] = $stat;
                        $this->cache[$path]['stat']['latest'] = $latest;
                }
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $stat;
        }
@@ -465,14 +526,17 @@ abstract class FileBackendStore extends FileBackend {
         */
        public function getFileContents( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $tmpFile = $this->getLocalReference( $params );
                if ( !$tmpFile ) {
+                       wfProfileOut( __METHOD__ . '-' . $this->name );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
                wfSuppressWarnings();
                $data = file_get_contents( $tmpFile->getPath() );
                wfRestoreWarnings();
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $data;
        }
@@ -483,18 +547,24 @@ abstract class FileBackendStore extends FileBackend {
         */
        final public function getFileSha1Base36( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $path = $params['src'];
                if ( isset( $this->cache[$path]['sha1'] ) ) {
+                       $this->pingCache( $path ); // LRU
+                       wfProfileOut( __METHOD__ . '-' . $this->name );
                        wfProfileOut( __METHOD__ );
                        return $this->cache[$path]['sha1'];
                }
                wfProfileIn( __METHOD__ . '-miss' );
+               wfProfileIn( __METHOD__ . '-miss-' . $this->name );
                $hash = $this->doGetFileSha1Base36( $params );
+               wfProfileOut( __METHOD__ . '-miss-' . $this->name );
                wfProfileOut( __METHOD__ . '-miss' );
                if ( $hash ) { // don't cache negatives
                        $this->trimCache(); // limit memory
                        $this->cache[$path]['sha1'] = $hash;
                }
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $hash;
        }
@@ -518,8 +588,10 @@ abstract class FileBackendStore extends FileBackend {
         */
        final public function getFileProps( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $fsFile = $this->getLocalReference( $params );
                $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $props;
        }
@@ -530,8 +602,11 @@ abstract class FileBackendStore extends FileBackend {
         */
        public function getLocalReference( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $path = $params['src'];
                if ( isset( $this->expensiveCache[$path]['localRef'] ) ) {
+                       $this->pingExpensiveCache( $path );
+                       wfProfileOut( __METHOD__ . '-' . $this->name );
                        wfProfileOut( __METHOD__ );
                        return $this->expensiveCache[$path]['localRef'];
                }
@@ -540,6 +615,7 @@ abstract class FileBackendStore extends FileBackend {
                        $this->trimExpensiveCache(); // limit memory
                        $this->expensiveCache[$path]['localRef'] = $tmpFile;
                }
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $tmpFile;
        }
@@ -550,6 +626,7 @@ abstract class FileBackendStore extends FileBackend {
         */
        final public function streamFile( array $params ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = Status::newGood();
 
                $info = $this->getFileStat( $params );
@@ -564,12 +641,15 @@ abstract class FileBackendStore extends FileBackend {
                        // do nothing; client cache is up to date
                } elseif ( $res == StreamFile::READY_STREAM ) {
                        wfProfileIn( __METHOD__ . '-send' );
+                       wfProfileIn( __METHOD__ . '-send-' . $this->name );
                        $status = $this->doStreamFile( $params );
+                       wfProfileOut( __METHOD__ . '-send-' . $this->name );
                        wfProfileOut( __METHOD__ . '-send' );
                } else {
                        $status->fatal( 'backend-fail-stream', $params['src'] );
                }
 
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -592,8 +672,79 @@ abstract class FileBackendStore extends FileBackend {
        }
 
        /**
-        * @copydoc FileBackend::getFileList()
-        * @return Array|null|Traversable
+        * @see FileBackend::directoryExists()
+        * @return bool|null
+        */
+       final public function directoryExists( array $params ) {
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) {
+                       return false; // invalid storage path
+               }
+               if ( $shard !== null ) { // confined to a single container/shard
+                       return $this->doDirectoryExists( $fullCont, $dir, $params );
+               } else { // directory is on several shards
+                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
+                       list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+                       $res = false; // response
+                       foreach ( $this->getContainerSuffixes( $shortCont ) as $suffix ) {
+                               $exists = $this->doDirectoryExists( "{$fullCont}{$suffix}", $dir, $params );
+                               if ( $exists ) {
+                                       $res = true;
+                                       break; // found one!
+                               } elseif ( $exists === null ) { // error?
+                                       $res = null; // if we don't find anything, it is indeterminate
+                               }
+                       }
+                       return $res;
+               }
+       }
+
+       /**
+        * @see FileBackendStore::directoryExists()
+        *
+        * @param $container string Resolved container name
+        * @param $dir string Resolved path relative to container
+        * @param $params Array
+        * @return bool|null
+        */
+       abstract protected function doDirectoryExists( $container, $dir, array $params );
+
+       /**
+        * @see FileBackend::getDirectoryList()
+        * @return Traversable|Array|null Returns null on failure
+        */
+       final public function getDirectoryList( array $params ) {
+               list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
+               if ( $dir === null ) { // invalid storage path
+                       return null;
+               }
+               if ( $shard !== null ) {
+                       // File listing is confined to a single container/shard
+                       return $this->getDirectoryListInternal( $fullCont, $dir, $params );
+               } else {
+                       wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
+                       // File listing spans multiple containers/shards
+                       list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
+                       return new FileBackendStoreShardDirIterator( $this,
+                               $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
+               }
+       }
+
+       /**
+        * Do not call this function from places outside FileBackend
+        *
+        * @see FileBackendStore::getDirectoryList()
+        *
+        * @param $container string Resolved container name
+        * @param $dir string Resolved path relative to container
+        * @param $params Array
+        * @return Traversable|Array|null Returns null on failure
+        */
+       abstract public function getDirectoryListInternal( $container, $dir, array $params );
+
+       /**
+        * @see FileBackend::getFileList()
+        * @return Traversable|Array|null Returns null on failure
         */
        final public function getFileList( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
@@ -607,7 +758,7 @@ abstract class FileBackendStore extends FileBackend {
                        wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
                        // File listing spans multiple containers/shards
                        list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
-                       return new FileBackendStoreShardListIterator( $this,
+                       return new FileBackendStoreShardFileIterator( $this,
                                $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
                }
        }
@@ -616,17 +767,17 @@ abstract class FileBackendStore extends FileBackend {
         * Do not call this function from places outside FileBackend
         *
         * @see FileBackendStore::getFileList()
-        * 
+        *
         * @param $container string Resolved container name
         * @param $dir string Resolved path relative to container
         * @param $params Array
-        * @return Traversable|Array|null
+        * @return Traversable|Array|null Returns null on failure
         */
        abstract public function getFileListInternal( $container, $dir, array $params );
 
        /**
         * Get the list of supported operations and their corresponding FileOp classes.
-        * 
+        *
         * @return Array
         */
        protected function supportedOperations() {
@@ -646,12 +797,12 @@ abstract class FileBackendStore extends FileBackend {
         *
         * The result must have the same number of items as the input.
         * An exception is thrown if an unsupported operation is requested.
-        * 
+        *
         * @param $ops Array Same format as doOperations()
         * @return Array List of FileOp objects
         * @throws MWException
         */
-       final public function getOperations( array $ops ) {
+       final public function getOperationsInternal( array $ops ) {
                $supportedOps = $this->supportedOperations();
 
                $performOps = array(); // array of FileOp objects
@@ -665,55 +816,76 @@ abstract class FileBackendStore extends FileBackend {
                                // Append the FileOp class
                                $performOps[] = new $class( $this, $params );
                        } else {
-                               throw new MWException( "Operation `$opName` is not supported." );
+                               throw new MWException( "Operation '$opName' is not supported." );
                        }
                }
 
                return $performOps;
        }
 
+       /**
+        * Get a list of storage paths to lock for a list of operations
+        * Returns an array with 'sh' (shared) and 'ex' (exclusive) keys,
+        * each corresponding to a list of storage paths to be locked.
+        *
+        * @param $performOps Array List of FileOp objects
+        * @return Array ('sh' => list of paths, 'ex' => list of paths)
+        */
+       final public function getPathsToLockForOpsInternal( array $performOps ) {
+               // Build up a list of files to lock...
+               $paths = array( 'sh' => array(), 'ex' => array() );
+               foreach ( $performOps as $fileOp ) {
+                       $paths['sh'] = array_merge( $paths['sh'], $fileOp->storagePathsRead() );
+                       $paths['ex'] = array_merge( $paths['ex'], $fileOp->storagePathsChanged() );
+               }
+               // Optimization: if doing an EX lock anyway, don't also set an SH one
+               $paths['sh'] = array_diff( $paths['sh'], $paths['ex'] );
+               // Get a shared lock on the parent directory of each path changed
+               $paths['sh'] = array_merge( $paths['sh'], array_map( 'dirname', $paths['ex'] ) );
+
+               return $paths;
+       }
+
        /**
         * @see FileBackend::doOperationsInternal()
         * @return Status
         */
        protected function doOperationsInternal( array $ops, array $opts ) {
                wfProfileIn( __METHOD__ );
+               wfProfileIn( __METHOD__ . '-' . $this->name );
                $status = Status::newGood();
 
                // Build up a list of FileOps...
-               $performOps = $this->getOperations( $ops );
+               $performOps = $this->getOperationsInternal( $ops );
 
                // Acquire any locks as needed...
                if ( empty( $opts['nonLocking'] ) ) {
                        // Build up a list of files to lock...
-                       $filesLockEx = $filesLockSh = array();
-                       foreach ( $performOps as $fileOp ) {
-                               $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() );
-                               $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() );
-                       }
-                       // Optimization: if doing an EX lock anyway, don't also set an SH one
-                       $filesLockSh = array_diff( $filesLockSh, $filesLockEx );
-                       // Get a shared lock on the parent directory of each path changed
-                       $filesLockSh = array_merge( $filesLockSh, array_map( 'dirname', $filesLockEx ) );
+                       $paths = $this->getPathsToLockForOpsInternal( $performOps );
                        // Try to lock those files for the scope of this function...
-                       $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
-                       $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
+                       $scopeLockS = $this->getScopedFileLocks( $paths['sh'], LockManager::LOCK_UW, $status );
+                       $scopeLockE = $this->getScopedFileLocks( $paths['ex'], LockManager::LOCK_EX, $status );
                        if ( !$status->isOK() ) {
+                               wfProfileOut( __METHOD__ . '-' . $this->name );
                                wfProfileOut( __METHOD__ );
                                return $status; // abort
                        }
                }
 
-               // Clear any cache entries (after locks acquired)
+               // Clear any file cache entries (after locks acquired)
                $this->clearCache();
 
+               // Load from the persistent container cache
+               $this->primeContainerCache( $performOps );
+
                // Actually attempt the operation batch...
-               $subStatus = FileOp::attemptBatch( $performOps, $opts );
+               $subStatus = FileOp::attemptBatch( $performOps, $opts, $this->fileJournal );
 
                // Merge errors into status fields
                $status->merge( $subStatus );
                $status->success = $subStatus->success; // not done in merge()
 
+               wfProfileOut( __METHOD__ . '-' . $this->name );
                wfProfileOut( __METHOD__ );
                return $status;
        }
@@ -740,17 +912,40 @@ abstract class FileBackendStore extends FileBackend {
 
        /**
         * Clears any additional stat caches for storage paths
-        * 
+        *
         * @see FileBackend::clearCache()
-        * 
+        *
         * @param $paths Array Storage paths (optional)
         * @return void
         */
        protected function doClearCache( array $paths = null ) {}
 
+       /**
+        * Is this a key/value store where directories are just virtual?
+        * Virtual directories exists in so much as files exists that are
+        * prefixed with the directory path followed by a forward slash.
+        *
+        * @return bool
+        */
+       abstract protected function directoriesAreVirtual();
+
+       /**
+        * Move a cache entry to the top (such as when accessed)
+        *
+        * @param $path string Storage path
+        * @return void
+        */
+       protected function pingCache( $path ) {
+               if ( isset( $this->cache[$path] ) ) {
+                       $tmp = $this->cache[$path];
+                       unset( $this->cache[$path] );
+                       $this->cache[$path] = $tmp;
+               }
+       }
+
        /**
         * Prune the inexpensive cache if it is too big to add an item
-        * 
+        *
         * @return void
         */
        protected function trimCache() {
@@ -760,9 +955,23 @@ abstract class FileBackendStore extends FileBackend {
                }
        }
 
+       /**
+        * Move a cache entry to the top (such as when accessed)
+        *
+        * @param $path string Storage path
+        * @return void
+        */
+       protected function pingExpensiveCache( $path ) {
+               if ( isset( $this->expensiveCache[$path] ) ) {
+                       $tmp = $this->expensiveCache[$path];
+                       unset( $this->expensiveCache[$path] );
+                       $this->expensiveCache[$path] = $tmp;
+               }
+       }
+
        /**
         * Prune the expensive cache if it is too big to add an item
-        * 
+        *
         * @return void
         */
        protected function trimExpensiveCache() {
@@ -775,12 +984,12 @@ abstract class FileBackendStore extends FileBackend {
        /**
         * Check if a container name is valid.
         * This checks for for length and illegal characters.
-        * 
+        *
         * @param $container string
         * @return bool
         */
        final protected static function isValidContainerName( $container ) {
-               // This accounts for Swift and S3 restrictions while leaving room 
+               // This accounts for Swift and S3 restrictions while leaving room
                // for things like '.xxx' (hex shard chars) or '.seg' (segments).
                // This disallows directory separators or traversal characters.
                // Note that matching strings URL encode to the same string;
@@ -879,6 +1088,19 @@ abstract class FileBackendStore extends FileBackend {
                return ''; // no sharding
        }
 
+       /**
+        * Check if a storage path maps to a single shard.
+        * Container dirs like "a", where the container shards on "x/xy",
+        * can reside on several shards. Such paths are tricky to handle.
+        *
+        * @param $storagePath string Storage path
+        * @return bool
+        */
+       final public function isSingleShardPathInternal( $storagePath ) {
+               list( $c, $r, $shard ) = $this->resolveStoragePath( $storagePath );
+               return ( $shard !== null );
+       }
+
        /**
         * Get the sharding config for a container.
         * If greater than 0, then all file storage paths within
@@ -903,9 +1125,9 @@ abstract class FileBackendStore extends FileBackend {
 
        /**
         * Get a list of full container shard suffixes for a container
-        * 
+        *
         * @param $container string
-        * @return Array 
+        * @return Array
         */
        final protected function getContainerSuffixes( $container ) {
                $shards = array();
@@ -921,9 +1143,9 @@ abstract class FileBackendStore extends FileBackend {
 
        /**
         * Get the full container name, including the wiki ID prefix
-        * 
+        *
         * @param $container string
-        * @return string 
+        * @return string
         */
        final protected function fullContainerName( $container ) {
                if ( $this->wikiId != '' ) {
@@ -937,9 +1159,9 @@ abstract class FileBackendStore extends FileBackend {
         * Resolve a container name, checking if it's allowed by the backend.
         * This is intended for internal use, such as encoding illegal chars.
         * Subclasses can override this to be more restrictive.
-        * 
+        *
         * @param $container string
-        * @return string|null 
+        * @return string|null
         */
        protected function resolveContainerName( $container ) {
                return $container;
@@ -958,29 +1180,113 @@ abstract class FileBackendStore extends FileBackend {
        protected function resolveContainerPath( $container, $relStoragePath ) {
                return $relStoragePath;
        }
+
+       /**
+        * Get the cache key for a container
+        *
+        * @param $container Resolved container name
+        * @return string
+        */
+       private function containerCacheKey( $container ) {
+               return wfMemcKey( 'backend', $this->getName(), 'container', $container );
+       }
+
+       /**
+        * Set the cached info for a container
+        *
+        * @param $container Resolved container name
+        * @param $val mixed Information to cache
+        * @return void
+        */
+       final protected function setContainerCache( $container, $val ) {
+               $this->memCache->set( $this->containerCacheKey( $container ), $val, 7*86400 );
+       }
+
+       /**
+        * Delete the cached info for a container
+        *
+        * @param $container Resolved container name
+        * @return void
+        */
+       final protected function deleteContainerCache( $container ) {
+               $this->memCache->delete( $this->containerCacheKey( $container ) );
+       }
+
+       /**
+        * Do a batch lookup from cache for container stats for all containers
+        * used in a list of container names, storage paths, or FileOp objects.
+        *
+        * @param $items Array List of storage paths or FileOps
+        * @return void
+        */
+       final protected function primeContainerCache( array $items ) {
+               $paths = array(); // list of storage paths
+               $contNames = array(); // (cache key => resolved container name)
+               // Get all the paths/containers from the items...
+               foreach ( $items as $item ) {
+                       if ( $item instanceof FileOp ) {
+                               $paths = array_merge( $paths, $item->storagePathsRead() );
+                               $paths = array_merge( $paths, $item->storagePathsChanged() );
+                       } elseif ( self::isStoragePath( $item ) ) {
+                               $paths[] = $item;
+                       } elseif ( is_string( $item ) ) { // full container name
+                               $contNames[$this->containerCacheKey( $item )] = $item;
+                       }
+               }
+               // Get all the corresponding cache keys for paths...
+               foreach ( $paths as $path ) {
+                       list( $fullCont, $r, $s ) = $this->resolveStoragePath( $path );
+                       if ( $fullCont !== null ) { // valid path for this backend
+                               $contNames[$this->containerCacheKey( $fullCont )] = $fullCont;
+                       }
+               }
+
+               $contInfo = array(); // (resolved container name => cache value)
+               // Get all cache entries for these container cache keys...
+               $values = $this->memCache->getBatch( array_keys( $contNames ) );
+               foreach ( $values as $cacheKey => $val ) {
+                       $contInfo[$contNames[$cacheKey]] = $val;
+               }
+
+               // Populate the container process cache for the backend...
+               $this->doPrimeContainerCache( array_filter( $contInfo, 'is_array' ) );
+       }
+
+       /**
+        * Fill the backend-specific process cache given an array of
+        * resolved container names and their corresponding cached info.
+        * Only containers that actually exist should appear in the map.
+        *
+        * @param $containerInfo Array Map of resolved container names to cached info
+        * @return void
+        */
+       protected function doPrimeContainerCache( array $containerInfo ) {}
 }
 
 /**
- * FileBackendStore helper function to handle file listings that span container shards.
+ * FileBackendStore helper function to handle listings that span container shards.
  * Do not use this class from places outside of FileBackendStore.
  *
  * @ingroup FileBackend
  */
-class FileBackendStoreShardListIterator implements Iterator {
-       /* @var FileBackendStore */
+abstract class FileBackendStoreShardListIterator implements Iterator {
+       /** @var FileBackendStore */
        protected $backend;
-       /* @var Array */
+       /** @var Array */
        protected $params;
-       /* @var Array */
+       /** @var Array */
        protected $shardSuffixes;
-       protected $container; // string
-       protected $directory; // string
+       protected $container; // string; full container name
+       protected $directory; // string; resolved relative path
 
-       /* @var Traversable */
+       /** @var Traversable */
        protected $iter;
        protected $curShard = 0; // integer
        protected $pos = 0; // integer
 
+       /** @var Array */
+       protected $multiShardPaths = array(); // (rel path => 1)
+
        /**
         * @param $backend FileBackendStore
         * @param $container string Full storage container name
@@ -1029,6 +1335,8 @@ class FileBackendStoreShardListIterator implements Iterator {
                } else {
                        $this->iter->next();
                }
+               // Filter out items that we already listed
+               $this->filterViaNext();
                // Find the next non-empty shard if no elements are left
                $this->nextShardIteratorIfNotValid();
        }
@@ -1041,6 +1349,8 @@ class FileBackendStoreShardListIterator implements Iterator {
                $this->pos = 0;
                $this->curShard = 0;
                $this->setIteratorFromCurrentShard();
+               // Filter out items that we already listed
+               $this->filterViaNext();
                // Find the next non-empty shard if this one has no elements
                $this->nextShardIteratorIfNotValid();
        }
@@ -1050,7 +1360,7 @@ class FileBackendStoreShardListIterator implements Iterator {
         * @return bool
         */
        public function valid() {
-               if ( $this->iter == null ) {
+               if ( $this->iter === null ) {
                        return false; // some failure?
                } elseif ( is_array( $this->iter ) ) {
                        return ( current( $this->iter ) !== false ); // no paths can have this value
@@ -1059,6 +1369,25 @@ class FileBackendStoreShardListIterator implements Iterator {
                }
        }
 
+       /**
+        * Filter out duplicate items by advancing to the next ones
+        */
+       protected function filterViaNext() {
+               while ( $this->iter->valid() ) {
+                       $rel = $this->iter->current(); // path relative to given directory
+                       $path = $this->params['dir'] . "/{$rel}"; // full storage path
+                       if ( !$this->backend->isSingleShardPathInternal( $path ) ) {
+                               // Don't keep listing paths that are on multiple shards
+                               if ( isset( $this->multiShardPaths[$rel] ) ) {
+                                       $this->iter->next(); // we already listed this path
+                               } else {
+                                       $this->multiShardPaths[$rel] = 1;
+                                       break;
+                               }
+                       }
+               }
+       }
+
        /**
         * If the list iterator for this container shard is out of items,
         * then move on to the next container that has items.
@@ -1078,7 +1407,35 @@ class FileBackendStoreShardListIterator implements Iterator {
         */
        protected function setIteratorFromCurrentShard() {
                $suffix = $this->shardSuffixes[$this->curShard];
-               $this->iter = $this->backend->getFileListInternal(
+               $this->iter = $this->listFromShard(
                        "{$this->container}{$suffix}", $this->directory, $this->params );
        }
+
+       /**
+        * Get the list for a given container shard
+        *
+        * @param $container string Resolved container name
+        * @param $dir string Resolved path relative to container
+        * @param $params Array
+        * @return Traversable|Array|null
+        */
+       abstract protected function listFromShard( $container, $dir, array $params );
+}
+
+/**
+ * Iterator for listing directories
+ */
+class FileBackendStoreShardDirIterator extends FileBackendStoreShardListIterator {
+       protected function listFromShard( $container, $dir, array $params ) {
+               return $this->backend->getDirectoryListInternal( $container, $dir, $params );
+       }
+}
+
+/**
+ * Iterator for listing regular files
+ */
+class FileBackendStoreShardFileIterator extends FileBackendStoreShardListIterator {
+       protected function listFromShard( $container, $dir, array $params ) {
+               return $this->backend->getFileListInternal( $container, $dir, $params );
+       }
 }