* Documentation fix
[lhc/web/wiklou.git] / includes / filerepo / backend / FileBackend.php
index d971098..eb9d4ae 100644 (file)
  * @ingroup FileBackend
  * @since 1.19
  */
-abstract class FileBackendBase {
-       protected $name; // unique backend name
-       protected $wikiId; // unique wiki name
-       protected $readOnly; // string
+abstract class FileBackend {
+       protected $name; // string; unique backend name
+       protected $wikiId; // string; unique wiki name
+       protected $readOnly; // string; read-only explanation message
        /** @var LockManager */
        protected $lockManager;
 
@@ -55,27 +55,14 @@ abstract class FileBackendBase {
                $this->wikiId = isset( $config['wikiId'] )
                        ? $config['wikiId']
                        : wfWikiID(); // e.g. "my_wiki-en_"
-               $this->wikiId = $this->resolveWikiId( $this->wikiId );
                $this->lockManager = LockManagerGroup::singleton()->get( $config['lockManager'] );
                $this->readOnly = isset( $config['readOnly'] )
                        ? (string)$config['readOnly']
                        : '';
        }
 
-       /**
-        * Normalize a wiki ID by replacing characters that are
-        * not supported by the backend as part of container names.
-        * 
-        * @param $wikiId string
-        * @return string 
-        */
-       protected function resolveWikiId( $wikiId ) {
-               return $wikiId;
-       }
-
        /**
         * Get the unique backend name.
-        * 
         * We may have multiple different backends of the same type.
         * For example, we can have two Swift backends using different proxies.
         * 
@@ -88,6 +75,7 @@ abstract class FileBackendBase {
        /**
         * This is the main entry point into the backend for write operations.
         * Callers supply an ordered list of operations to perform as a transaction.
+        * Files will be locked, the stat cache cleared, and then the operations attempted.
         * If any serious errors occur, all attempted operations will be rolled back.
         * 
         * $ops is an array of arrays. The outer array holds a list of operations.
@@ -99,7 +87,7 @@ abstract class FileBackendBase {
         *         'op'                  => 'create',
         *         'dst'                 => <storage path>,
         *         'content'             => <string of new file contents>,
-        *         'overwriteDest'       => <boolean>,
+        *         'overwrite'           => <boolean>,
         *         'overwriteSame'       => <boolean>
         *     )
         * b) Copy a file system file into storage
@@ -107,7 +95,7 @@ abstract class FileBackendBase {
         *         'op'                  => 'store',
         *         'src'                 => <file system path>,
         *         'dst'                 => <storage path>,
-        *         'overwriteDest'       => <boolean>,
+        *         'overwrite'           => <boolean>,
         *         'overwriteSame'       => <boolean>
         *     )
         * c) Copy a file within storage
@@ -115,7 +103,7 @@ abstract class FileBackendBase {
         *         'op'                  => 'copy',
         *         'src'                 => <storage path>,
         *         'dst'                 => <storage path>,
-        *         'overwriteDest'       => <boolean>,
+        *         'overwrite'           => <boolean>,
         *         'overwriteSame'       => <boolean>
         *     )
         * d) Move a file within storage
@@ -123,7 +111,7 @@ abstract class FileBackendBase {
         *         'op'                  => 'move',
         *         'src'                 => <storage path>,
         *         'dst'                 => <storage path>,
-        *         'overwriteDest'       => <boolean>,
+        *         'overwrite'           => <boolean>,
         *         'overwriteSame'       => <boolean>
         *     )
         * e) Delete a file within storage
@@ -140,7 +128,7 @@ abstract class FileBackendBase {
         * Boolean flags for operations (operation-specific):
         * 'ignoreMissingSource' : The operation will simply succeed and do
         *                         nothing if the source file does not exist.
-        * 'overwriteDest'       : Any destination file will be overwritten.
+        * 'overwrite'           : Any destination file will be overwritten.
         * 'overwriteSame'       : An error will not be given if a file already
         *                         exists at the destination that has the same
         *                         contents as the new contents to be written there.
@@ -155,6 +143,11 @@ abstract class FileBackendBase {
         *                         This can increase performance for non-critical writes.
         *                         This has no effect unless the 'force' flag is set.
         * 
+        * Remarks on locking:
+        * File system paths given to operations should refer to files that are
+        * already locked or otherwise safe from modification from other processes.
+        * Normally these files will be new temp files, which should be adequate.
+        * 
         * Return value:
         * This returns a Status, which contains all warnings and fatals that occured
         * during the operation. The 'failCount', 'successCount', and 'success' members
@@ -177,7 +170,7 @@ abstract class FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::doOperations()
+        * @see FileBackend::doOperations()
         */
        abstract protected function doOperationsInternal( array $ops, array $opts );
 
@@ -186,7 +179,7 @@ abstract class FileBackendBase {
         * If you are doing a batch of operations that should either
         * all succeed or all fail, then use that function instead.
         *
-        * @see FileBackendBase::doOperations()
+        * @see FileBackend::doOperations()
         *
         * @param $op Array Operation
         * @param $opts Array Operation options
@@ -200,7 +193,7 @@ abstract class FileBackendBase {
         * Performs a single create operation.
         * This sets $params['op'] to 'create' and passes it to doOperation().
         *
-        * @see FileBackendBase::doOperation()
+        * @see FileBackend::doOperation()
         *
         * @param $params Array Operation parameters
         * @param $opts Array Operation options
@@ -215,7 +208,7 @@ abstract class FileBackendBase {
         * Performs a single store operation.
         * This sets $params['op'] to 'store' and passes it to doOperation().
         *
-        * @see FileBackendBase::doOperation()
+        * @see FileBackend::doOperation()
         *
         * @param $params Array Operation parameters
         * @param $opts Array Operation options
@@ -230,7 +223,7 @@ abstract class FileBackendBase {
         * Performs a single copy operation.
         * This sets $params['op'] to 'copy' and passes it to doOperation().
         *
-        * @see FileBackendBase::doOperation()
+        * @see FileBackend::doOperation()
         *
         * @param $params Array Operation parameters
         * @param $opts Array Operation options
@@ -245,7 +238,7 @@ abstract class FileBackendBase {
         * Performs a single move operation.
         * This sets $params['op'] to 'move' and passes it to doOperation().
         *
-        * @see FileBackendBase::doOperation()
+        * @see FileBackend::doOperation()
         *
         * @param $params Array Operation parameters
         * @param $opts Array Operation options
@@ -260,7 +253,7 @@ abstract class FileBackendBase {
         * Performs a single delete operation.
         * This sets $params['op'] to 'delete' and passes it to doOperation().
         *
-        * @see FileBackendBase::doOperation()
+        * @see FileBackend::doOperation()
         *
         * @param $params Array Operation parameters
         * @param $opts Array Operation options
@@ -272,7 +265,10 @@ abstract class FileBackendBase {
        }
 
        /**
-        * Concatenate a list of storage files into a single file on the file system
+        * Concatenate a list of storage files into a single file system file.
+        * The target path should refer to a file that is already locked or
+        * otherwise safe from modification from other processes. Normally,
+        * the file will be a new temp file, which should be adequate.
         * $params include:
         *     srcs          : ordered source storage paths (e.g. chunk1, chunk2, ...)
         *     dst           : file system path to 0-byte temp file
@@ -283,8 +279,9 @@ abstract class FileBackendBase {
        abstract public function concatenate( array $params );
 
        /**
-        * Prepare a storage path for usage. This will create containers
-        * that don't yet exist or, on FS backends, create parent directories.
+        * Prepare a storage directory for usage.
+        * This will create any required containers and parent directories.
+        * Backends using key/value stores only need to create the container.
         * 
         * $params include:
         *     dir : storage directory
@@ -300,15 +297,15 @@ abstract class FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::prepare()
+        * @see FileBackend::prepare()
         */
        abstract protected function doPrepare( array $params );
 
        /**
-        * Take measures to block web access to a directory and
+        * Take measures to block web access to a storage directory and
         * the container it belongs to. FS backends might add .htaccess
-        * files wheras backends like Swift this might restrict container
-        * access to backend user that represents end-users in web request.
+        * files whereas key/value store backends might restrict container
+        * access to the auth user that represents end-users in web request.
         * This is not guaranteed to actually do anything.
         * 
         * $params include:
@@ -323,17 +320,22 @@ abstract class FileBackendBase {
                if ( $this->readOnly != '' ) {
                        return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
                }
-               return $this->doSecure( $params );
+               $status = $this->doPrepare( $params ); // dir must exist to restrict it
+               if ( $status->isOK() ) {
+                       $status->merge( $this->doSecure( $params ) );
+               }
+               return $status;
        }
 
        /**
-        * @see FileBackendBase::secure()
+        * @see FileBackend::secure()
         */
        abstract protected function doSecure( array $params );
 
        /**
-        * Clean up an empty storage directory.
-        * On FS backends, the directory will be deleted. Others may do nothing.
+        * Delete a storage directory if it is empty.
+        * Backends using key/value stores may do nothing unless the directory
+        * is that of an empty container, in which case it should be deleted.
         * 
         * $params include:
         *     dir : storage directory
@@ -349,7 +351,7 @@ abstract class FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::clean()
+        * @see FileBackend::clean()
         */
        abstract protected function doClean( array $params );
 
@@ -409,6 +411,7 @@ abstract class FileBackendBase {
         * Otherwise, the result is an associative array that includes:
         *     mtime  : the last-modified timestamp (TS_MW)
         *     size   : the file size (bytes)
+        * Additional values may be included for internal use only.
         * 
         * $params include:
         *     src    : source storage path
@@ -472,6 +475,7 @@ abstract class FileBackendBase {
         * Write operations should *never* be done on this file as some backends
         * may do internal tracking or may be instances of FileBackendMultiWrite.
         * In that later case, there are copies of the file that must stay in sync.
+        * Additionally, further calls to this function may return the same file.
         * 
         * $params include:
         *     src    : source storage path
@@ -514,10 +518,10 @@ abstract class FileBackendBase {
         * Invalidate any in-process file existence and property cache.
         * If $paths is given, then only the cache for those files will be cleared.
         *
-        * @param $paths Array Storage paths
+        * @param $paths Array Storage paths (optional)
         * @return void
         */
-       abstract public function clearCache( array $paths = null );
+       public function clearCache( array $paths = null ) {}
 
        /**
         * Lock the files at the given storage paths in the backend.
@@ -560,27 +564,163 @@ abstract class FileBackendBase {
        final public function getScopedFileLocks( array $paths, $type, Status $status ) {
                return ScopedLock::factory( $this->lockManager, $paths, $type, $status );
        }
+
+       /**
+        * Check if a given path is a "mwstore://" path.
+        * This does not do any further validation or any existence checks.
+        * 
+        * @param $path string
+        * @return bool
+        */
+       final public static function isStoragePath( $path ) {
+               return ( strpos( $path, 'mwstore://' ) === 0 );
+       }
+
+       /**
+        * Split a storage path into a backend name, a container name, 
+        * and a relative file path. The relative path may be the empty string.
+        * This does not do any path normalization or traversal checks.
+        *
+        * @param $storagePath string
+        * @return Array (backend, container, rel object) or (null, null, null)
+        */
+       final public static function splitStoragePath( $storagePath ) {
+               if ( self::isStoragePath( $storagePath ) ) {
+                       // Remove the "mwstore://" prefix and split the path
+                       $parts = explode( '/', substr( $storagePath, 10 ), 3 );
+                       if ( count( $parts ) >= 2 && $parts[0] != '' && $parts[1] != '' ) {
+                               if ( count( $parts ) == 3 ) {
+                                       return $parts; // e.g. "backend/container/path"
+                               } else {
+                                       return array( $parts[0], $parts[1], '' ); // e.g. "backend/container" 
+                               }
+                       }
+               }
+               return array( null, null, null );
+       }
+
+       /**
+        * Normalize a storage path by cleaning up directory separators.
+        * Returns null if the path is not of the format of a valid storage path.
+        * 
+        * @param $storagePath string
+        * @return string|null 
+        */
+       final public static function normalizeStoragePath( $storagePath ) {
+               list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
+               if ( $relPath !== null ) { // must be for this backend
+                       $relPath = self::normalizeContainerPath( $relPath );
+                       if ( $relPath !== null ) {
+                               return ( $relPath != '' )
+                                       ? "mwstore://{$backend}/{$container}/{$relPath}"
+                                       : "mwstore://{$backend}/{$container}";
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Validate and normalize a relative storage path.
+        * Null is returned if the path involves directory traversal.
+        * Traversal is insecure for FS backends and broken for others.
+        *
+        * @param $path string Storage path relative to a container
+        * @return string|null
+        */
+       final protected static function normalizeContainerPath( $path ) {
+               // Normalize directory separators
+               $path = strtr( $path, '\\', '/' );
+               // Collapse any consecutive directory separators
+               $path = preg_replace( '![/]{2,}!', '/', $path );
+               // Remove any leading directory separator
+               $path = ltrim( $path, '/' );
+               // Use the same traversal protection as Title::secureAndSplit()
+               if ( strpos( $path, '.' ) !== false ) {
+                       if (
+                               $path === '.' ||
+                               $path === '..' ||
+                               strpos( $path, './' ) === 0 ||
+                               strpos( $path, '../' ) === 0 ||
+                               strpos( $path, '/./' ) !== false ||
+                               strpos( $path, '/../' ) !== false
+                       ) { 
+                               return null;
+                       }
+               }
+               return $path;
+       }
+
+       /**
+        * Get the parent storage directory of a storage path.
+        * This returns a path like "mwstore://backend/container",
+        * "mwstore://backend/container/...", or null if there is no parent.
+        * 
+        * @param $storagePath string
+        * @return string|null
+        */
+       final public static function parentStoragePath( $storagePath ) {
+               $storagePath = dirname( $storagePath );
+               list( $b, $cont, $rel ) = self::splitStoragePath( $storagePath );
+               return ( $rel === null ) ? null : $storagePath;
+       }
+
+       /**
+        * Get the final extension from a storage or FS path
+        * 
+        * @param $path string
+        * @return string
+        */
+       final public static function extensionFromPath( $path ) {
+               $i = strrpos( $path, '.' );
+               return strtolower( $i ? substr( $path, $i + 1 ) : '' );
+       }
 }
 
 /**
  * Base class for all single-write backends.
  * This class defines the methods as abstract that subclasses must implement.
- * Callers outside of FileBackend and its helper classes, such as FileOp,
- * should only call functions that are present in FileBackendBase.
- *
- * The FileBackendBase operations are implemented using primitive functions
+ * 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 FileBackend extends FileBackendBase {
-       /** @var Array */
+abstract class FileBackendStore extends FileBackend {
+       /** @var Array Map of paths to small (RAM/disk) cache items */
        protected $cache = array(); // (storage path => key => value)
-       protected $maxCacheSize = 75; // integer; max paths with entries
-       /** @var Array */
-       protected $shardViaHashLevels = array(); // (container name => integer)
+       protected $maxCacheSize = 100; // integer; max paths with entries
+       /** @var Array Map of paths to large (RAM/disk) cache items */
+       protected $expCache = array(); // (storage path => key => value)
+       protected $maxExpCacheSize = 10; // integer; max paths with entries
+
+       /** @var Array Map of container names to sharding settings */
+       protected $shardViaHashLevels = array(); // (container name => config array)
+
+       protected $maxFileSize = 1000000000; // integer bytes (1GB)
+
+       /**
+        * 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 
+        */
+       final public function maxFileSizeInternal() {
+               return $this->maxFileSize;
+       }
+
+       /**
+        * Check if a file can be created at a given storage path.
+        * FS backends should check if the parent directory exists and the file is writable.
+        * Backends using key/value stores should check if the container exists.
+        *
+        * @param $storagePath string
+        * @return bool
+        */
+       abstract public function isPathUsableInternal( $storagePath );
 
        /**
         * Create a file in the backend with the given contents.
@@ -589,19 +729,25 @@ abstract class FileBackend extends FileBackendBase {
         * $params include:
         *     content       : the raw file contents
         *     dst           : destination storage path
-        *     overwriteDest : overwrite any file that exists at the destination
+        *     overwrite     : overwrite any file that exists at the destination
         * 
         * @param $params Array
         * @return Status
         */
        final public function createInternal( array $params ) {
-               $status = $this->doCreateInternal( $params );
-               $this->clearCache( array( $params['dst'] ) );
+               wfProfileIn( __METHOD__ );
+               if ( strlen( $params['content'] ) > $this->maxFileSizeInternal() ) {
+                       $status = Status::newFatal( 'backend-fail-create', $params['dst'] );
+               } else {
+                       $status = $this->doCreateInternal( $params );
+                       $this->clearCache( array( $params['dst'] ) );
+               }
+               wfProfileOut( __METHOD__ );
                return $status;
        }
 
        /**
-        * @see FileBackend::createInternal()
+        * @see FileBackendStore::createInternal()
         */
        abstract protected function doCreateInternal( array $params );
 
@@ -612,19 +758,25 @@ abstract class FileBackend extends FileBackendBase {
         * $params include:
         *     src           : source path on disk
         *     dst           : destination storage path
-        *     overwriteDest : overwrite any file that exists at the destination
+        *     overwrite     : overwrite any file that exists at the destination
         * 
         * @param $params Array
         * @return Status
         */
        final public function storeInternal( array $params ) {
-               $status = $this->doStoreInternal( $params );
-               $this->clearCache( array( $params['dst'] ) );
+               wfProfileIn( __METHOD__ );
+               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__ );
                return $status;
        }
 
        /**
-        * @see FileBackend::storeInternal()
+        * @see FileBackendStore::storeInternal()
         */
        abstract protected function doStoreInternal( array $params );
 
@@ -635,19 +787,21 @@ abstract class FileBackend extends FileBackendBase {
         * $params include:
         *     src           : source storage path
         *     dst           : destination storage path
-        *     overwriteDest : overwrite any file that exists at the destination
+        *     overwrite     : overwrite any file that exists at the destination
         * 
         * @param $params Array
         * @return Status
         */
        final public function copyInternal( array $params ) {
+               wfProfileIn( __METHOD__ );
                $status = $this->doCopyInternal( $params );
                $this->clearCache( array( $params['dst'] ) );
+               wfProfileOut( __METHOD__ );
                return $status;
        }
 
        /**
-        * @see FileBackend::copyInternal()
+        * @see FileBackendStore::copyInternal()
         */
        abstract protected function doCopyInternal( array $params );
 
@@ -663,13 +817,15 @@ abstract class FileBackend extends FileBackendBase {
         * @return Status
         */
        final public function deleteInternal( array $params ) {
+               wfProfileIn( __METHOD__ );
                $status = $this->doDeleteInternal( $params );
                $this->clearCache( array( $params['src'] ) );
+               wfProfileOut( __METHOD__ );
                return $status;
        }
 
        /**
-        * @see FileBackend::deleteInternal()
+        * @see FileBackendStore::deleteInternal()
         */
        abstract protected function doDeleteInternal( array $params );
 
@@ -680,52 +836,53 @@ abstract class FileBackend extends FileBackendBase {
         * $params include:
         *     src           : source storage path
         *     dst           : destination storage path
-        *     overwriteDest : overwrite any file that exists at the destination
+        *     overwrite     : overwrite any file that exists at the destination
         * 
         * @param $params Array
         * @return Status
         */
        final public function moveInternal( array $params ) {
+               wfProfileIn( __METHOD__ );
                $status = $this->doMoveInternal( $params );
                $this->clearCache( array( $params['src'], $params['dst'] ) );
+               wfProfileOut( __METHOD__ );
                return $status;
        }
 
        /**
-        * @see FileBackend::moveInternal()
+        * @see FileBackendStore::moveInternal()
         */
        protected function doMoveInternal( array $params ) {
                // Copy source to dest
                $status = $this->copyInternal( $params );
-               if ( !$status->isOK() ) {
-                       return $status;
+               if ( $status->isOK() ) {
+                       // Delete source (only fails due to races or medium going down)
+                       $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) );
+                       $status->setResult( true, $status->value ); // ignore delete() errors
                }
-               // Delete source (only fails due to races or medium going down)
-               $status->merge( $this->deleteInternal( array( 'src' => $params['src'] ) ) );
-               $status->setResult( true, $status->value ); // ignore delete() errors
                return $status;
        }
 
        /**
-        * @see FileBackendBase::concatenate()
+        * @see FileBackend::concatenate()
         */
        final public function concatenate( array $params ) {
+               wfProfileIn( __METHOD__ );
                $status = Status::newGood();
 
                // Try to lock the source files for the scope of this function
                $scopeLockS = $this->getScopedFileLocks( $params['srcs'], LockManager::LOCK_UW, $status );
-               if ( !$status->isOK() ) {
-                       return $status; // abort
+               if ( $status->isOK() ) {
+                       // Actually do the concatenation
+                       $status->merge( $this->doConcatenate( $params ) );
                }
 
-               // Actually do the concatenation
-               $status->merge( $this->doConcatenate( $params ) );
-
+               wfProfileOut( __METHOD__ );
                return $status;
        }
 
        /**
-        * @see FileBackend::concatenate()
+        * @see FileBackendStore::concatenate()
         */
        protected function doConcatenate( array $params ) {
                $status = Status::newGood();
@@ -741,7 +898,7 @@ abstract class FileBackend extends FileBackendBase {
                }
 
                // Build up the temp file using the source chunks (in order)...
-               $tmpHandle = fopen( $tmpPath, 'a' );
+               $tmpHandle = fopen( $tmpPath, 'ab' );
                if ( $tmpHandle === false ) {
                        $status->fatal( 'backend-fail-opentemp', $tmpPath );
                        return $status;
@@ -774,19 +931,25 @@ abstract class FileBackend extends FileBackendBase {
                        return $status;
                }
 
+               clearstatcache(); // temp file changed
+
                return $status;
        }
 
        /**
-        * @see FileBackendBase::doPrepare()
+        * @see FileBackend::doPrepare()
         */
        final protected function doPrepare( array $params ) {
+               wfProfileIn( __METHOD__ );
+
                $status = Status::newGood();
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
+                       wfProfileOut( __METHOD__ );
                        return $status; // invalid storage path
                }
+
                if ( $shard !== null ) { // confined to a single container/shard
                        $status->merge( $this->doPrepareInternal( $fullCont, $dir, $params ) );
                } else { // directory is on several shards
@@ -796,26 +959,32 @@ abstract class FileBackend extends FileBackendBase {
                                $status->merge( $this->doPrepareInternal( "{$fullCont}{$suffix}", $dir, $params ) );
                        }
                }
+
+               wfProfileOut( __METHOD__ );
                return $status;
        }
 
        /**
-        * @see FileBackend::doPrepare()
+        * @see FileBackendStore::doPrepare()
         */
        protected function doPrepareInternal( $container, $dir, array $params ) {
                return Status::newGood();
        }
 
        /**
-        * @see FileBackendBase::doSecure()
+        * @see FileBackend::doSecure()
         */
        final protected function doSecure( array $params ) {
+               wfProfileIn( __METHOD__ );
                $status = Status::newGood();
+
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
+                       wfProfileOut( __METHOD__ );
                        return $status; // invalid storage path
                }
+
                if ( $shard !== null ) { // confined to a single container/shard
                        $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
                } else { // directory is on several shards
@@ -825,26 +994,40 @@ abstract class FileBackend extends FileBackendBase {
                                $status->merge( $this->doSecureInternal( "{$fullCont}{$suffix}", $dir, $params ) );
                        }
                }
+
+               wfProfileOut( __METHOD__ );
                return $status;
        }
 
        /**
-        * @see FileBackend::doSecure()
+        * @see FileBackendStore::doSecure()
         */
        protected function doSecureInternal( $container, $dir, array $params ) {
                return Status::newGood();
        }
 
        /**
-        * @see FileBackendBase::doClean()
+        * @see FileBackend::doClean()
         */
        final protected function doClean( array $params ) {
+               wfProfileIn( __METHOD__ );
                $status = Status::newGood();
+
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
                if ( $dir === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
+                       wfProfileOut( __METHOD__ );
                        return $status; // invalid storage path
                }
+
+               // Attempt to lock this directory...
+               $filesLockEx = array( $params['dir'] );
+               $scopedLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
+               if ( !$status->isOK() ) {
+                       wfProfileOut( __METHOD__ );
+                       return $status; // abort
+               }
+
                if ( $shard !== null ) { // confined to a single container/shard
                        $status->merge( $this->doCleanInternal( $fullCont, $dir, $params ) );
                } else { // directory is on several shards
@@ -854,92 +1037,106 @@ abstract class FileBackend extends FileBackendBase {
                                $status->merge( $this->doCleanInternal( "{$fullCont}{$suffix}", $dir, $params ) );
                        }
                }
+
+               wfProfileOut( __METHOD__ );
                return $status;
        }
 
        /**
-        * @see FileBackend::doClean()
+        * @see FileBackendStore::doClean()
         */
        protected function doCleanInternal( $container, $dir, array $params ) {
                return Status::newGood();
        }
 
        /**
-        * @see FileBackendBase::fileExists()
+        * @see FileBackend::fileExists()
         */
        final public function fileExists( array $params ) {
+               wfProfileIn( __METHOD__ );
                $stat = $this->getFileStat( $params );
-               if ( $stat === null ) {
-                       return null; // failure
-               }
-               return (bool)$stat;
+               wfProfileOut( __METHOD__ );
+               return ( $stat === null ) ? null : (bool)$stat; // null => failure
        }
 
        /**
-        * @see FileBackendBase::getFileTimestamp()
+        * @see FileBackend::getFileTimestamp()
         */
        final public function getFileTimestamp( array $params ) {
+               wfProfileIn( __METHOD__ );
                $stat = $this->getFileStat( $params );
-               if ( $stat ) {
-                       return $stat['mtime'];
-               } else {
-                       return false;
-               }
+               wfProfileOut( __METHOD__ );
+               return $stat ? $stat['mtime'] : false;
        }
 
        /**
-        * @see FileBackendBase::getFileSize()
+        * @see FileBackend::getFileSize()
         */
        final public function getFileSize( array $params ) {
+               wfProfileIn( __METHOD__ );
                $stat = $this->getFileStat( $params );
-               if ( $stat ) {
-                       return $stat['size'];
-               } else {
-                       return false;
-               }
+               wfProfileOut( __METHOD__ );
+               return $stat ? $stat['size'] : false;
        }
 
        /**
-        * @see FileBackendBase::getFileStat()
+        * @see FileBackend::getFileStat()
         */
        final public function getFileStat( array $params ) {
-               $path = $params['src'];
+               wfProfileIn( __METHOD__ );
+               $path = self::normalizeStoragePath( $params['src'] );
+               if ( $path === null ) {
+                       return false; // invalid storage path
+               }
+               $latest = !empty( $params['latest'] );
                if ( isset( $this->cache[$path]['stat'] ) ) {
-                       return $this->cache[$path]['stat'];
+                       // 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'] ) {
+                               wfProfileOut( __METHOD__ );
+                               return $this->cache[$path]['stat'];
+                       }
                }
                $stat = $this->doGetFileStat( $params );
                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__ );
                return $stat;
        }
 
        /**
-        * @see FileBackend::getFileStat()
+        * @see FileBackendStore::getFileStat()
         */
        abstract protected function doGetFileStat( array $params );
 
        /**
-        * @see FileBackendBase::getFileContents()
+        * @see FileBackend::getFileContents()
         */
        public function getFileContents( array $params ) {
+               wfProfileIn( __METHOD__ );
                $tmpFile = $this->getLocalReference( $params );
                if ( !$tmpFile ) {
+                       wfProfileOut( __METHOD__ );
                        return false;
                }
                wfSuppressWarnings();
                $data = file_get_contents( $tmpFile->getPath() );
                wfRestoreWarnings();
+               wfProfileOut( __METHOD__ );
                return $data;
        }
 
        /**
-        * @see FileBackendBase::getFileSha1Base36()
+        * @see FileBackend::getFileSha1Base36()
         */
        final public function getFileSha1Base36( array $params ) {
+               wfProfileIn( __METHOD__ );
                $path = $params['src'];
                if ( isset( $this->cache[$path]['sha1'] ) ) {
+                       wfProfileOut( __METHOD__ );
                        return $this->cache[$path]['sha1'];
                }
                $hash = $this->doGetFileSha1Base36( $params );
@@ -947,11 +1144,12 @@ abstract class FileBackend extends FileBackendBase {
                        $this->trimCache(); // limit memory
                        $this->cache[$path]['sha1'] = $hash;
                }
+               wfProfileOut( __METHOD__ );
                return $hash;
        }
 
        /**
-        * @see FileBackend::getFileSha1Base36()
+        * @see FileBackendStore::getFileSha1Base36()
         */
        protected function doGetFileSha1Base36( array $params ) {
                $fsFile = $this->getLocalReference( $params );
@@ -963,37 +1161,40 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::getFileProps()
+        * @see FileBackend::getFileProps()
         */
-       public function getFileProps( array $params ) {
+       final public function getFileProps( array $params ) {
+               wfProfileIn( __METHOD__ );
                $fsFile = $this->getLocalReference( $params );
-               if ( !$fsFile ) {
-                       return FSFile::placeholderProps();
-               } else {
-                       return $fsFile->getProps();
-               }
+               $props = $fsFile ? $fsFile->getProps() : FSFile::placeholderProps();
+               wfProfileOut( __METHOD__ );
+               return $props;
        }
 
        /**
-        * @see FileBackendBase::getLocalReference()
+        * @see FileBackend::getLocalReference()
         */
        public function getLocalReference( array $params ) {
+               wfProfileIn( __METHOD__ );
                $path = $params['src'];
-               if ( isset( $this->cache[$path]['localRef'] ) ) {
-                       return $this->cache[$path]['localRef'];
+               if ( isset( $this->expCache[$path]['localRef'] ) ) {
+                       wfProfileOut( __METHOD__ );
+                       return $this->expCache[$path]['localRef'];
                }
                $tmpFile = $this->getLocalCopy( $params );
                if ( $tmpFile ) { // don't cache negatives
-                       $this->trimCache(); // limit memory
-                       $this->cache[$path]['localRef'] = $tmpFile;
+                       $this->trimExpCache(); // limit memory
+                       $this->expCache[$path]['localRef'] = $tmpFile;
                }
+               wfProfileOut( __METHOD__ );
                return $tmpFile;
        }
 
        /**
-        * @see FileBackendBase::streamFile()
+        * @see FileBackend::streamFile()
         */
        final public function streamFile( array $params ) {
+               wfProfileIn( __METHOD__ );
                $status = Status::newGood();
 
                $info = $this->getFileStat( $params );
@@ -1012,11 +1213,12 @@ abstract class FileBackend extends FileBackendBase {
                        $status->fatal( 'backend-fail-stream', $params['src'] );
                }
 
+               wfProfileOut( __METHOD__ );
                return $status;
        }
 
        /**
-        * @see FileBackend::streamFile()
+        * @see FileBackendStore::streamFile()
         */
        protected function doStreamFile( array $params ) {
                $status = Status::newGood();
@@ -1032,7 +1234,7 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::getFileList()
+        * @see FileBackend::getFileList()
         */
        final public function getFileList( array $params ) {
                list( $fullCont, $dir, $shard ) = $this->resolveStoragePath( $params['dir'] );
@@ -1046,18 +1248,20 @@ abstract class FileBackend extends FileBackendBase {
                        wfDebug( __METHOD__ . ": iterating over all container shards.\n" );
                        // File listing spans multiple containers/shards
                        list( $b, $shortCont, $r ) = self::splitStoragePath( $params['dir'] );
-                       return new ContainerShardListIterator( $this,
-                               $fullCont, $this->getContainerSuffixes( $shortCont ), $params );
+                       return new FileBackendStoreShardListIterator( $this,
+                               $fullCont, $dir, $this->getContainerSuffixes( $shortCont ), $params );
                }
        }
 
        /**
-        * Do not call this function from places outside FileBackend and ContainerFileListIterator
+        * 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
-        * @see FileBackend::getFileList()
+        * @return Traversable|Array|null
         */
        abstract public function getFileListInternal( $container, $dir, array $params );
 
@@ -1110,9 +1314,10 @@ abstract class FileBackend extends FileBackendBase {
        }
 
        /**
-        * @see FileBackendBase::doOperationsInternal()
+        * @see FileBackend::doOperationsInternal()
         */
        protected function doOperationsInternal( array $ops, array $opts ) {
+               wfProfileIn( __METHOD__ );
                $status = Status::newGood();
 
                // Build up a list of FileOps...
@@ -1128,10 +1333,13 @@ abstract class FileBackend extends FileBackendBase {
                        }
                        // 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 ) );
                        // 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 );
                        if ( !$status->isOK() ) {
+                               wfProfileOut( __METHOD__ );
                                return $status; // abort
                        }
                }
@@ -1146,64 +1354,62 @@ abstract class FileBackend extends FileBackendBase {
                $status->merge( $subStatus );
                $status->success = $subStatus->success; // not done in merge()
 
+               wfProfileOut( __METHOD__ );
                return $status;
        }
 
        /**
-        * @see FileBackendBase::clearCache()
+        * @see FileBackend::clearCache()
         */
        final public function clearCache( array $paths = null ) {
+               if ( is_array( $paths ) ) {
+                       $paths = array_map( 'FileBackend::normalizeStoragePath', $paths );
+                       $paths = array_filter( $paths, 'strlen' ); // remove nulls
+               }
                if ( $paths === null ) {
                        $this->cache = array();
+                       $this->expCache = array();
                } else {
                        foreach ( $paths as $path ) {
                                unset( $this->cache[$path] );
+                               unset( $this->expCache[$path] );
                        }
                }
+               $this->doClearCache( $paths );
        }
 
        /**
-        * Prune the cache if it is too big to add an item
+        * 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 ) {}
+
+       /**
+        * Prune the inexpensive cache if it is too big to add an item
         * 
         * @return void
         */
        protected function trimCache() {
                if ( count( $this->cache ) >= $this->maxCacheSize ) {
                        reset( $this->cache );
-                       $key = key( $this->cache );
-                       unset( $this->cache[$key] );
+                       unset( $this->cache[key( $this->cache )] );
                }
        }
 
        /**
-        * Check if a given path is a mwstore:// path.
-        * This does not do any actual validation or existence checks.
+        * Prune the expensive cache if it is too big to add an item
         * 
-        * @param $path string
-        * @return bool
-        */
-       final public static function isStoragePath( $path ) {
-               return ( strpos( $path, 'mwstore://' ) === 0 );
-       }
-
-       /**
-        * Split a storage path (e.g. "mwstore://backend/container/path/to/object")
-        * into a backend name, a container name, and a relative object path.
-        *
-        * @param $storagePath string
-        * @return Array (backend, container, rel object) or (null, null, null)
+        * @return void
         */
-       final public static function splitStoragePath( $storagePath ) {
-               if ( self::isStoragePath( $storagePath ) ) {
-                       // Note: strlen( 'mwstore://' ) = 10
-                       $parts = explode( '/', substr( $storagePath, 10 ), 3 );
-                       if ( count( $parts ) == 3 ) {
-                               return $parts; // e.g. "backend/container/path"
-                       } elseif ( count( $parts ) == 2 ) {
-                               return array( $parts[0], $parts[1], '' ); // e.g. "backend/container" 
-                       }
+       protected function trimExpCache() {
+               if ( count( $this->expCache ) >= $this->maxExpCacheSize ) {
+                       reset( $this->expCache );
+                       unset( $this->expCache[key( $this->expCache )] );
                }
-               return array( null, null, null );
        }
 
        /**
@@ -1216,38 +1422,12 @@ abstract class FileBackend extends FileBackendBase {
        final protected static function isValidContainerName( $container ) {
                // 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;
-               // in Swift, the length resriction is *after* URL encoding.
+               // in Swift, the length restriction is *after* URL encoding.
                return preg_match( '/^[a-z0-9][a-z0-9-_]{0,199}$/i', $container );
        }
 
-       /**
-        * Validate and normalize a relative storage path.
-        * Null is returned if the path involves directory traversal.
-        * Traversal is insecure for FS backends and broken for others.
-        *
-        * @param $path string
-        * @return string|null
-        */
-       final protected static function normalizeStoragePath( $path ) {
-               // Normalize directory separators
-               $path = strtr( $path, '\\', '/' );
-               // Use the same traversal protection as Title::secureAndSplit()
-               if ( strpos( $path, '.' ) !== false ) {
-                       if (
-                               $path === '.' ||
-                               $path === '..' ||
-                               strpos( $path, './' ) === 0 ||
-                               strpos( $path, '../' ) === 0 ||
-                               strpos( $path, '/./' ) !== false ||
-                               strpos( $path, '/../' ) !== false
-                       ) { 
-                               return null;
-                       }
-               }
-               return $path;
-       }
-
        /**
         * Splits a storage path into an internal container name,
         * an internal relative file name, and a container shard suffix.
@@ -1264,7 +1444,7 @@ abstract class FileBackend extends FileBackendBase {
        final protected function resolveStoragePath( $storagePath ) {
                list( $backend, $container, $relPath ) = self::splitStoragePath( $storagePath );
                if ( $backend === $this->name ) { // must be for this backend
-                       $relPath = self::normalizeStoragePath( $relPath );
+                       $relPath = self::normalizeContainerPath( $relPath );
                        if ( $relPath !== null ) {
                                // Get shard for the normalized path if this container is sharded
                                $cShard = $this->getContainerShard( $container, $relPath );
@@ -1290,7 +1470,7 @@ abstract class FileBackend extends FileBackendBase {
         * Like resolveStoragePath() except null values are returned if
         * the container is sharded and the shard could not be determined.
         *
-        * @see FileBackend::resolveStoragePath()
+        * @see FileBackendStore::resolveStoragePath()
         *
         * @param $storagePath string
         * @return Array (container, path) or (null, null) if invalid
@@ -1312,39 +1492,53 @@ abstract class FileBackend extends FileBackendBase {
         * @return string|null Returns null if shard could not be determined
         */
        final protected function getContainerShard( $container, $relPath ) {
-               $hashLevels = $this->getContainerHashLevels( $container );
-               if ( $hashLevels === 1 ) { // 16 shards per container
-                       $hashDirRegex = '(?P<shard>[0-9a-f])';
-               } elseif ( $hashLevels === 2 ) { // 256 shards per container
-                       $hashDirRegex = '[0-9a-f]/(?P<shard>[0-9a-f]{2})';
-               } else {
-                       return ''; // no sharding
-               }
-               // Allow certain directories to be above the hash dirs so as
-               // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
-               // They must be 2+ chars to avoid any hash directory ambiguity.
-               if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
-                       return '.' . str_pad( $m['shard'], $hashLevels, '0', STR_PAD_LEFT );
+               list( $levels, $base, $repeat ) = $this->getContainerHashLevels( $container );
+               if ( $levels == 1 || $levels == 2 ) {
+                       // Hash characters are either base 16 or 36
+                       $char = ( $base == 36 ) ? '[0-9a-z]' : '[0-9a-f]';
+                       // Get a regex that represents the shard portion of paths.
+                       // The concatenation of the captures gives us the shard.
+                       if ( $levels === 1 ) { // 16 or 36 shards per container
+                               $hashDirRegex = '(' . $char . ')';
+                       } else { // 256 or 1296 shards per container
+                               if ( $repeat ) { // verbose hash dir format (e.g. "a/ab/abc")
+                                       $hashDirRegex = $char . '/(' . $char . '{2})';
+                               } else { // short hash dir format (e.g. "a/b/c")
+                                       $hashDirRegex = '(' . $char . ')/(' . $char . ')';
+                               }
+                       }
+                       // Allow certain directories to be above the hash dirs so as
+                       // to work with FileRepo (e.g. "archive/a/ab" or "temp/a/ab").
+                       // They must be 2+ chars to avoid any hash directory ambiguity.
+                       $m = array();
+                       if ( preg_match( "!^(?:[^/]{2,}/)*$hashDirRegex(?:/|$)!", $relPath, $m ) ) {
+                               return '.' . implode( '', array_slice( $m, 1 ) );
+                       }
+                       return null; // failed to match
                }
-               return null; // failed to match
+               return ''; // no sharding
        }
 
        /**
-        * Get the number of hash levels for a container.
+        * Get the sharding config for a container.
         * If greater than 0, then all file storage paths within
         * the container are required to be hashed accordingly.
         *
         * @param $container string
-        * @return integer
+        * @return Array (integer levels, integer base, repeat flag) or (0, 0, false)
         */
        final protected function getContainerHashLevels( $container ) {
                if ( isset( $this->shardViaHashLevels[$container] ) ) {
-                       $hashLevels = (int)$this->shardViaHashLevels[$container];
-                       if ( $hashLevels >= 0 && $hashLevels <= 2 ) {
-                               return $hashLevels;
+                       $config = $this->shardViaHashLevels[$container];
+                       $hashLevels = (int)$config['levels'];
+                       if ( $hashLevels == 0 || $hashLevels == 2 ) {
+                               $hashBase = (int)$config['base'];
+                               if ( $hashBase == 16 || $hashBase == 36 ) {
+                                       return array( $hashLevels, $hashBase, $config['repeat'] );
+                               }
                        }
                }
-               return 0; // no sharding
+               return array( 0, 0, false ); // no sharding
        }
 
        /**
@@ -1355,11 +1549,11 @@ abstract class FileBackend extends FileBackendBase {
         */
        final protected function getContainerSuffixes( $container ) {
                $shards = array();
-               $digits = $this->getContainerHashLevels( $container );
+               list( $digits, $base ) = $this->getContainerHashLevels( $container );
                if ( $digits > 0 ) {
-                       $numShards = 1 << ( $digits * 4 );
+                       $numShards = pow( $base, $digits );
                        for ( $index = 0; $index < $numShards; $index++ ) {
-                               $shards[] = '.' . str_pad( dechex( $index ), $digits, '0', STR_PAD_LEFT );
+                               $shards[] = '.' . wfBaseConvert( $index, 10, $base, $digits );
                        }
                }
                return $shards;
@@ -1404,27 +1598,16 @@ abstract class FileBackend extends FileBackendBase {
        protected function resolveContainerPath( $container, $relStoragePath ) {
                return $relStoragePath;
        }
-
-       /**
-        * Get the final extension from a storage or FS path
-        * 
-        * @param $path string
-        * @return string
-        */
-       final public static function extensionFromPath( $path ) {
-               $i = strrpos( $path, '.' );
-               return strtolower( $i ? substr( $path, $i + 1 ) : '' );
-       }
 }
 
 /**
- * FileBackend helper function to handle file listings that span container shards.
- * Do not use this class from places outside of FileBackend.
+ * FileBackendStore helper function to handle file listings that span container shards.
+ * Do not use this class from places outside of FileBackendStore.
  *
- * @ingroup FileBackend
+ * @ingroup FileBackendStore
  */
-class ContainerShardListIterator implements Iterator {
-       /* @var FileBackend */
+class FileBackendStoreShardListIterator implements Iterator {
+       /* @var FileBackendStore */
        protected $backend;
        /* @var Array */
        protected $params;
@@ -1439,14 +1622,14 @@ class ContainerShardListIterator implements Iterator {
        protected $pos = 0; // integer
 
        /**
-        * @param $backend FileBackend
+        * @param $backend FileBackendStore
         * @param $container string Full storage container name
         * @param $dir string Storage directory relative to container
         * @param $suffixes Array List of container shard suffixes
         * @param $params Array
         */
        public function __construct(
-               FileBackend $backend, $container, $dir, array $suffixes, array $params
+               FileBackendStore $backend, $container, $dir, array $suffixes, array $params
        ) {
                $this->backend = $backend;
                $this->container = $container;