Quick fix to getContainerHashLevels() comparison from r110435.
[lhc/web/wiklou.git] / includes / filerepo / backend / FSFileBackend.php
index 7c1cca4..3eabd29 100644 (file)
@@ -6,29 +6,46 @@
  */
 
 /**
- * Class for a file system based file backend.
- * Containers are just directories and container sharding is not supported.
- * Also, for backwards-compatibility, the wiki ID prefix is not used.
- * Users of this class should set wiki-specific container paths as needed.
+ * Class for a file system (FS) based file backend.
+ * 
+ * All "containers" each map to a directory under the backend's base directory.
+ * For backwards-compatibility, some container paths can be set to custom paths.
+ * The wiki ID will not be used in any custom paths, so this should be avoided.
+ * 
+ * Having directories with thousands of files will diminish performance.
+ * Sharding can be accomplished by using FileRepo-style hash paths.
  *
  * Status messages should avoid mentioning the internal FS paths.
  * Likewise, error suppression should be used to avoid path disclosure.
  *
  * @ingroup FileBackend
+ * @since 1.19
  */
-class FSFileBackend extends FileBackend {
-       /** @var Array Map of container names to paths */
-       protected $containerPaths = array();
-       protected $fileMode; // file permission mode
+class FSFileBackend extends FileBackendStore {
+       protected $basePath; // string; directory holding the container directories
+       /** @var Array Map of container names to root paths */
+       protected $containerPaths = array(); // for custom container paths
+       protected $fileMode; // integer; file permission mode
+
+       protected $hadWarningErrors = array();
 
        /**
-        * @see FileBackend::__construct()
+        * @see FileBackendStore::__construct()
         * Additional $config params include:
-        *    containerPaths : Map of container names to absolute file system paths
-        *    fileMode       : Octal UNIX file permissions to use on files stored
+        *    basePath       : File system directory that holds containers.
+        *    containerPaths : Map of container names to custom file system directories.
+        *                     This should only be used for backwards-compatibility.
+        *    fileMode       : Octal UNIX file permissions to use on files stored.
         */
        public function __construct( array $config ) {
                parent::__construct( $config );
+               if ( isset( $config['basePath'] ) ) {
+                       if ( substr( $this->basePath, -1 ) === '/' ) {
+                               $this->basePath = substr( $this->basePath, 0, -1 ); // remove trailing slash
+                       }
+               } else {
+                       $this->basePath = null; // none; containers must have explicit paths
+               }
                $this->containerPaths = (array)$config['containerPaths'];
                foreach ( $this->containerPaths as &$path ) {
                        if ( substr( $path, -1 ) === '/' ) {
@@ -41,29 +58,86 @@ class FSFileBackend extends FileBackend {
        }
 
        /**
-        * @see FileBackend::resolveContainerPath()
+        * @see FileBackendStore::resolveContainerPath()
         */
        protected function resolveContainerPath( $container, $relStoragePath ) {
-               // Get absolute path given the container base dir
-               if ( isset( $this->containerPaths[$container] ) ) {
-                       return $this->containerPaths[$container] . "/{$relStoragePath}";
+               if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) {
+                       return $relStoragePath; // container has a root directory
                }
                return null;
        }
 
        /**
-        * @see FileBackend::doStoreInternal()
+        * Given the short (unresolved) and full (resolved) name of
+        * a container, return the file system path of the container.
+        * 
+        * @param $shortCont string
+        * @param $fullCont string
+        * @return string|null 
+        */
+       protected function containerFSRoot( $shortCont, $fullCont ) {
+               if ( isset( $this->containerPaths[$shortCont] ) ) {
+                       return $this->containerPaths[$shortCont]; 
+               } elseif ( isset( $this->basePath ) ) {
+                       return "{$this->basePath}/{$fullCont}";
+               }
+               return null; // no container base path defined
+       }
+
+       /**
+        * Get the absolute file system path for a storage path
+        * 
+        * @param $storagePath string Storage path
+        * @return string|null
+        */
+       protected function resolveToFSPath( $storagePath ) {
+               list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
+               if ( $relPath === null ) {
+                       return null; // invalid
+               }
+               list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $storagePath );
+               $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               if ( $relPath != '' ) {
+                       $fsPath .= "/{$relPath}";
+               }
+               return $fsPath;
+       }
+
+       /**
+        * @see FileBackendStore::isPathUsableInternal()
+        */
+       public function isPathUsableInternal( $storagePath ) {
+               $fsPath = $this->resolveToFSPath( $storagePath );
+               if ( $fsPath === null ) {
+                       return false; // invalid
+               }
+               $parentDir = dirname( $fsPath );
+
+               wfSuppressWarnings();
+               if ( file_exists( $fsPath ) ) {
+                       $ok = is_file( $fsPath ) && is_writable( $fsPath );
+               } else {
+                       $ok = is_dir( $parentDir ) && is_writable( $parentDir );
+               }
+               wfRestoreWarnings();
+
+               return $ok;
+       }
+
+       /**
+        * @see FileBackendStore::doStoreInternal()
         */
        protected function doStoreInternal( array $params ) {
                $status = Status::newGood();
 
-               list( $c, $dest ) = $this->resolveStoragePathReal( $params['dst'] );
+               $dest = $this->resolveToFSPath( $params['dst'] );
                if ( $dest === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
                        return $status;
                }
+
                if ( file_exists( $dest ) ) {
-                       if ( !empty( $params['overwriteDest'] ) ) {
+                       if ( !empty( $params['overwrite'] ) ) {
                                wfSuppressWarnings();
                                $ok = unlink( $dest );
                                wfRestoreWarnings();
@@ -75,18 +149,13 @@ class FSFileBackend extends FileBackend {
                                $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
                                return $status;
                        }
-               } else {
-                       if ( !wfMkdirParents( dirname( $dest ) ) ) {
-                               $status->fatal( 'directorycreateerror', $params['dst'] );
-                               return $status;
-                       }
                }
 
                wfSuppressWarnings();
                $ok = copy( $params['src'], $dest );
                wfRestoreWarnings();
                if ( !$ok ) {
-                       $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
+                       $status->fatal( 'backend-fail-store', $params['src'], $params['dst'] );
                        return $status;
                }
 
@@ -96,25 +165,25 @@ class FSFileBackend extends FileBackend {
        }
 
        /**
-        * @see FileBackend::doCopyInternal()
+        * @see FileBackendStore::doCopyInternal()
         */
        protected function doCopyInternal( array $params ) {
                $status = Status::newGood();
 
-               list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] );
+               $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['src'] );
                        return $status;
                }
 
-               list( $c, $dest ) = $this->resolveStoragePathReal( $params['dst'] );
+               $dest = $this->resolveToFSPath( $params['dst'] );
                if ( $dest === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
                        return $status;
                }
 
                if ( file_exists( $dest ) ) {
-                       if ( !empty( $params['overwriteDest'] ) ) {
+                       if ( !empty( $params['overwrite'] ) ) {
                                wfSuppressWarnings();
                                $ok = unlink( $dest );
                                wfRestoreWarnings();
@@ -126,11 +195,6 @@ class FSFileBackend extends FileBackend {
                                $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
                                return $status;
                        }
-               } else {
-                       if ( !wfMkdirParents( dirname( $dest ) ) ) {
-                               $status->fatal( 'directorycreateerror', $params['dst'] );
-                               return $status;
-                       }
                }
 
                wfSuppressWarnings();
@@ -147,24 +211,25 @@ class FSFileBackend extends FileBackend {
        }
 
        /**
-        * @see FileBackend::doMoveInternal()
+        * @see FileBackendStore::doMoveInternal()
         */
        protected function doMoveInternal( array $params ) {
                $status = Status::newGood();
 
-               list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] );
+               $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['src'] );
                        return $status;
                }
-               list( $c, $dest ) = $this->resolveStoragePathReal( $params['dst'] );
+
+               $dest = $this->resolveToFSPath( $params['dst'] );
                if ( $dest === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
                        return $status;
                }
 
                if ( file_exists( $dest ) ) {
-                       if ( !empty( $params['overwriteDest'] ) ) {
+                       if ( !empty( $params['overwrite'] ) ) {
                                // Windows does not support moving over existing files
                                if ( wfIsWindows() ) {
                                        wfSuppressWarnings();
@@ -179,11 +244,6 @@ class FSFileBackend extends FileBackend {
                                $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
                                return $status;
                        }
-               } else {
-                       if ( !wfMkdirParents( dirname( $dest ) ) ) {
-                               $status->fatal( 'directorycreateerror', $params['dst'] );
-                               return $status;
-                       }
                }
 
                wfSuppressWarnings();
@@ -199,12 +259,12 @@ class FSFileBackend extends FileBackend {
        }
 
        /**
-        * @see FileBackend::doDeleteInternal()
+        * @see FileBackendStore::doDeleteInternal()
         */
        protected function doDeleteInternal( array $params ) {
                $status = Status::newGood();
 
-               list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] );
+               $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['src'] );
                        return $status;
@@ -229,19 +289,19 @@ class FSFileBackend extends FileBackend {
        }
 
        /**
-        * @see FileBackend::doCreateInternal()
+        * @see FileBackendStore::doCreateInternal()
         */
        protected function doCreateInternal( array $params ) {
                $status = Status::newGood();
 
-               list( $c, $dest ) = $this->resolveStoragePathReal( $params['dst'] );
+               $dest = $this->resolveToFSPath( $params['dst'] );
                if ( $dest === null ) {
                        $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
                        return $status;
                }
 
                if ( file_exists( $dest ) ) {
-                       if ( !empty( $params['overwriteDest'] ) ) {
+                       if ( !empty( $params['overwrite'] ) ) {
                                wfSuppressWarnings();
                                $ok = unlink( $dest );
                                wfRestoreWarnings();
@@ -253,11 +313,6 @@ class FSFileBackend extends FileBackend {
                                $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
                                return $status;
                        }
-               } else {
-                       if ( !wfMkdirParents( dirname( $dest ) ) ) {
-                               $status->fatal( 'directorycreateerror', $params['dst'] );
-                               return $status;
-                       }
                }
 
                wfSuppressWarnings();
@@ -274,11 +329,14 @@ class FSFileBackend extends FileBackend {
        }
 
        /**
-        * @see FileBackend::doPrepare()
+        * @see FileBackendStore::doPrepareInternal()
         */
-       protected function doPrepare( $container, $dir, array $params ) {
+       protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
                $status = Status::newGood();
-               if ( !wfMkdirParents( $dir ) ) {
+               list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
+               if ( !wfMkdirParents( $dir ) ) { // make directory and its parents
                        $status->fatal( 'directorycreateerror', $params['dir'] );
                } elseif ( !is_writable( $dir ) ) {
                        $status->fatal( 'directoryreadonlyerror', $params['dir'] );
@@ -289,14 +347,13 @@ class FSFileBackend extends FileBackend {
        }
 
        /**
-        * @see FileBackend::doSecure()
+        * @see FileBackendStore::doSecureInternal()
         */
-       protected function doSecure( $container, $dir, array $params ) {
+       protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
                $status = Status::newGood();
-               if ( !wfMkdirParents( $dir ) ) {
-                       $status->fatal( 'directorycreateerror', $params['dir'] );
-                       return $status;
-               }
+               list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
                // Seed new directories with a blank index.html, to prevent crawling...
                if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
                        wfSuppressWarnings();
@@ -308,26 +365,29 @@ class FSFileBackend extends FileBackend {
                        }
                }
                // Add a .htaccess file to the root of the container...
-               list( $b, $container, $r ) = FileBackend::splitStoragePath( $params['dir'] );
-               $dirRoot = $this->containerPaths[$container]; // real path
-               if ( !empty( $params['noAccess'] ) && !file_exists( "{$dirRoot}/.htaccess" ) ) {
-                       wfSuppressWarnings();
-                       $ok = file_put_contents( "{$dirRoot}/.htaccess", "Deny from all\n" );
-                       wfRestoreWarnings();
-                       if ( !$ok ) {
-                               $storeDir = "mwstore://{$this->name}/{$container}";
-                               $status->fatal( 'backend-fail-create', "$storeDir/.htaccess" );
-                               return $status;
+               if ( !empty( $params['noAccess'] ) ) {
+                       if ( !file_exists( "{$contRoot}/.htaccess" ) ) {
+                               wfSuppressWarnings();
+                               $ok = file_put_contents( "{$contRoot}/.htaccess", "Deny from all\n" );
+                               wfRestoreWarnings();
+                               if ( !$ok ) {
+                                       $storeDir = "mwstore://{$this->name}/{$shortCont}";
+                                       $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
+                                       return $status;
+                               }
                        }
                }
                return $status;
        }
 
        /**
-        * @see FileBackend::doClean()
+        * @see FileBackendStore::doCleanInternal()
         */
-       protected function doClean( $container, $dir, array $params ) {
+       protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
                $status = Status::newGood();
+               list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
                wfSuppressWarnings();
                if ( is_dir( $dir ) ) {
                        rmdir( $dir ); // remove directory if empty
@@ -337,36 +397,44 @@ class FSFileBackend extends FileBackend {
        }
 
        /**
-        * @see FileBackend::doFileExists()
+        * @see FileBackendStore::doFileExists()
         */
        protected function doGetFileStat( array $params ) {
-               list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] );
+               $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
                        return false; // invalid storage path
                }
 
-               wfSuppressWarnings();
-               if ( is_file( $source ) ) { // regular file?
-                       $stat = stat( $source );
-               } else {
-                       $stat = false;
-               }
-               wfRestoreWarnings();
+               $this->trapWarnings();
+               $stat = is_file( $source ) ? stat( $source ) : false; // regular files only
+               $hadError = $this->untrapWarnings();
 
                if ( $stat ) {
                        return array(
                                'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
                                'size'  => $stat['size']
                        );
+               } elseif ( !$hadError ) {
+                       return false; // file does not exist
                } else {
-                       return false;
+                       return null; // failure
                }
        }
 
        /**
-        * @see FileBackend::getFileListInternal()
+        * @see FileBackendStore::doClearCache()
+        */
+       protected function doClearCache( array $paths = null ) {
+               clearstatcache(); // clear the PHP file stat cache
+       }
+
+       /**
+        * @see FileBackendStore::getFileListInternal()
         */
-       public function getFileListInternal( $container, $dir, array $params ) {
+       public function getFileListInternal( $fullCont, $dirRel, array $params ) {
+               list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] );
+               $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
+               $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot;
                wfSuppressWarnings();
                $exists = is_dir( $dir );
                wfRestoreWarnings();
@@ -379,14 +447,14 @@ class FSFileBackend extends FileBackend {
                if ( !$readable ) {
                        return null; // bad permissions?
                }
-               return new FSFileIterator( $dir );
+               return new FSFileBackendFileList( $dir );
        }
 
        /**
-        * @see FileBackend::getLocalReference()
+        * @see FileBackendStore::getLocalReference()
         */
        public function getLocalReference( array $params ) {
-               list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] );
+               $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
                        return null;
                }
@@ -394,10 +462,10 @@ class FSFileBackend extends FileBackend {
        }
 
        /**
-        * @see FileBackend::getLocalCopy()
+        * @see FileBackendStore::getLocalCopy()
         */
        public function getLocalCopy( array $params ) {
-               list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] );
+               $source = $this->resolveToFSPath( $params['src'] );
                if ( $source === null ) {
                        return null;
                }
@@ -436,30 +504,65 @@ class FSFileBackend extends FileBackend {
 
                return $ok;
        }
+
+       /**
+        * Suppress E_WARNING errors and track whether any happen
+        *
+        * @return void
+        */
+       protected function trapWarnings() {
+               $this->hadWarningErrors[] = false; // push to stack
+               set_error_handler( array( $this, 'handleWarning' ), E_WARNING );
+       }
+
+       /**
+        * Unsuppress E_WARNING errors and return true if any happened
+        *
+        * @return bool
+        */
+       protected function untrapWarnings() {
+               restore_error_handler(); // restore previous handler
+               return array_pop( $this->hadWarningErrors ); // pop from stack
+       }
+
+       private function handleWarning() {
+               $this->hadWarningErrors[count( $this->hadWarningErrors ) - 1] = true;
+               return true; // suppress from PHP handler
+       }
 }
 
 /**
  * Wrapper around RecursiveDirectoryIterator that catches
  * exception or does any custom behavoir that we may want.
+ * Do not use this class from places outside FSFileBackend.
  *
  * @ingroup FileBackend
  */
-class FSFileIterator implements Iterator {
+class FSFileBackendFileList implements Iterator {
        /** @var RecursiveIteratorIterator */
        protected $iter;
        protected $suffixStart; // integer
+       protected $pos = 0; // integer
 
        /**
-        * Get an FSFileIterator from a file system directory
-        * 
-        * @param $dir string
+        * @param $dir string file system directory
         */
        public function __construct( $dir ) {
-               $this->suffixStart = strlen( realpath( $dir ) ) + 1; // size of "path/to/dir/"
+               $dir = realpath( $dir ); // normalize
+               $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
                try {
-                       $flags = FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS;
-                       $this->iter = new RecursiveIteratorIterator(
-                               new RecursiveDirectoryIterator( $dir, $flags ) );
+                       # Get an iterator that will return leaf nodes (non-directories)
+                       if ( MWInit::classExists( 'FilesystemIterator' ) ) { // PHP >= 5.3
+                               # RecursiveDirectoryIterator extends FilesystemIterator.
+                               # FilesystemIterator::SKIP_DOTS default is inconsistent in PHP 5.3.x.
+                               $flags = FilesystemIterator::CURRENT_AS_FILEINFO | FilesystemIterator::SKIP_DOTS;
+                               $this->iter = new RecursiveIteratorIterator( 
+                                       new RecursiveDirectoryIterator( $dir, $flags ) );
+                       } else { // PHP < 5.3
+                               # RecursiveDirectoryIterator extends DirectoryIterator
+                               $this->iter = new RecursiveIteratorIterator( 
+                                       new RecursiveDirectoryIterator( $dir ) );
+                       }
                } catch ( UnexpectedValueException $e ) {
                        $this->iter = null; // bad permissions? deleted?
                }
@@ -467,11 +570,13 @@ class FSFileIterator implements Iterator {
 
        public function current() {
                // Return only the relative path and normalize slashes to FileBackend-style
-               return str_replace( '\\', '/', substr( $this->iter->current(), $this->suffixStart ) );
+               // Make sure to use the realpath since the suffix is based upon that
+               return str_replace( '\\', '/',
+                       substr( realpath( $this->iter->current() ), $this->suffixStart ) );
        }
 
        public function key() {
-               return $this->iter->key();
+               return $this->pos;
        }
 
        public function next() {
@@ -480,9 +585,11 @@ class FSFileIterator implements Iterator {
                } catch ( UnexpectedValueException $e ) {
                        $this->iter = null;
                }
+               ++$this->pos;
        }
 
        public function rewind() {
+               $this->pos = 0;
                try {
                        $this->iter->rewind();
                } catch ( UnexpectedValueException $e ) {