*/
/**
- * Class for a file system based file backend.
+ * 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.
*/
- function __construct( array $config ) {
+ 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 ) === '/' ) {
}
/**
- * @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->resolveStoragePath( $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();
$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;
}
}
/**
- * @see FileBackend::doCopyInternal()
+ * @see FileBackendStore::doCopyInternal()
*/
protected function doCopyInternal( array $params ) {
$status = Status::newGood();
- list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
+ $source = $this->resolveToFSPath( $params['src'] );
if ( $source === null ) {
$status->fatal( 'backend-fail-invalidpath', $params['src'] );
return $status;
}
- list( $c, $dest ) = $this->resolveStoragePath( $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();
$status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
return $status;
}
- } else {
- if ( !wfMkdirParents( dirname( $dest ) ) ) {
- $status->fatal( 'directorycreateerror', $params['dst'] );
- return $status;
- }
}
wfSuppressWarnings();
}
/**
- * @see FileBackend::doMoveInternal()
+ * @see FileBackendStore::doMoveInternal()
*/
protected function doMoveInternal( array $params ) {
$status = Status::newGood();
- list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
+ $source = $this->resolveToFSPath( $params['src'] );
if ( $source === null ) {
$status->fatal( 'backend-fail-invalidpath', $params['src'] );
return $status;
}
- list( $c, $dest ) = $this->resolveStoragePath( $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();
$status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
return $status;
}
- } else {
- if ( !wfMkdirParents( dirname( $dest ) ) ) {
- $status->fatal( 'directorycreateerror', $params['dst'] );
- return $status;
- }
}
wfSuppressWarnings();
}
/**
- * @see FileBackend::doDeleteInternal()
+ * @see FileBackendStore::doDeleteInternal()
*/
protected function doDeleteInternal( array $params ) {
$status = Status::newGood();
- list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
+ $source = $this->resolveToFSPath( $params['src'] );
if ( $source === null ) {
$status->fatal( 'backend-fail-invalidpath', $params['src'] );
return $status;
}
/**
- * @see FileBackend::doCreateInternal()
+ * @see FileBackendStore::doCreateInternal()
*/
protected function doCreateInternal( array $params ) {
$status = Status::newGood();
- list( $c, $dest ) = $this->resolveStoragePath( $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();
$status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
return $status;
}
- } else {
- if ( !wfMkdirParents( dirname( $dest ) ) ) {
- $status->fatal( 'directorycreateerror', $params['dst'] );
- return $status;
- }
}
wfSuppressWarnings();
}
/**
- * @see FileBackend::prepare()
+ * @see FileBackendStore::doPrepareInternal()
*/
- function prepare( array $params ) {
+ protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
$status = Status::newGood();
- list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
- return $status; // invalid storage path
- }
- 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'] );
- return $status;
} elseif ( !is_writable( $dir ) ) {
$status->fatal( 'directoryreadonlyerror', $params['dir'] );
- return $status;
} elseif ( !is_readable( $dir ) ) {
$status->fatal( 'directorynotreadableerror', $params['dir'] );
- return $status;
}
return $status;
}
/**
- * @see FileBackend::secure()
+ * @see FileBackendStore::doSecureInternal()
*/
- function secure( array $params ) {
+ protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
$status = Status::newGood();
- list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
- return $status; // invalid storage path
- }
- if ( !wfMkdirParents( $dir ) ) {
- $status->fatal( 'directorycreateerror', $params['dir'] );
- return $status;
- }
- // Add a .htaccess file to the root of the deleted zone
- if ( !empty( $params['noAccess'] ) && !file_exists( "{$dir}/.htaccess" ) ) {
- wfSuppressWarnings();
- $ok = file_put_contents( "{$dir}/.htaccess", "Deny from all\n" );
- wfRestoreWarnings();
- if ( !$ok ) {
- $status->fatal( 'backend-fail-create', $params['dir'] . '/.htaccess' );
- return $status;
- }
- }
- // Seed new directories with a blank index.html, to prevent crawling
+ 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();
$ok = file_put_contents( "{$dir}/index.html", '' );
return $status;
}
}
+ // Add a .htaccess file to the root of the container...
+ 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::clean()
+ * @see FileBackendStore::doCleanInternal()
*/
- function clean( array $params ) {
+ protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
$status = Status::newGood();
- list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) {
- $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
- return $status; // invalid storage path
- }
+ 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
}
/**
- * @see FileBackend::fileExists()
+ * @see FileBackendStore::doFileExists()
*/
- function fileExists( array $params ) {
- list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
+ protected function doGetFileStat( array $params ) {
+ $source = $this->resolveToFSPath( $params['src'] );
if ( $source === null ) {
return false; // invalid storage path
}
- wfSuppressWarnings();
- $exists = is_file( $source );
- wfRestoreWarnings();
- return $exists;
+
+ $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 null; // failure
+ }
}
/**
- * @see FileBackend::getFileTimestamp()
+ * @see FileBackendStore::doClearCache()
*/
- function getFileTimestamp( array $params ) {
- list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
- if ( $source === null ) {
- return false; // invalid storage path
- }
- $fsFile = new FSFile( $source );
- return $fsFile->getTimestamp();
+ protected function doClearCache( array $paths = null ) {
+ clearstatcache(); // clear the PHP file stat cache
}
/**
- * @see FileBackend::getFileList()
+ * @see FileBackendStore::getFileListInternal()
*/
- function getFileList( array $params ) {
- list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
- if ( $dir === null ) { // invalid storage path
- return null;
- }
+ 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();
if ( !$readable ) {
return null; // bad permissions?
}
- return new FSFileIterator( $dir );
+ return new FSFileBackendFileList( $dir );
}
/**
- * @see FileBackend::getLocalReference()
+ * @see FileBackendStore::getLocalReference()
*/
- function getLocalReference( array $params ) {
- list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
+ public function getLocalReference( array $params ) {
+ $source = $this->resolveToFSPath( $params['src'] );
if ( $source === null ) {
return null;
}
}
/**
- * @see FileBackend::getLocalCopy()
+ * @see FileBackendStore::getLocalCopy()
*/
- function getLocalCopy( array $params ) {
- list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
+ public function getLocalCopy( array $params ) {
+ $source = $this->resolveToFSPath( $params['src'] );
if ( $source === null ) {
return null;
}
- // Get source file extension
- $i = strrpos( $source, '.' );
- $ext = strtolower( $i ? substr( $source, $i + 1 ) : '' );
- // Create a new temporary file...
+ // Create a new temporary file with the same extension...
+ $ext = FileBackend::extensionFromPath( $params['src'] );
$tmpFile = TempFSFile::factory( wfBaseName( $source ) . '_', $ext );
if ( !$tmpFile ) {
return null;
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 ) {
+ $dir = realpath( $dir ); // normalize
+ $this->suffixStart = strlen( $dir ) + 1; // size of "path/to/dir/"
try {
- $this->iter = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) );
+ # 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?
}
}
public function current() {
- return $this->iter->current();
+ // Return only the relative path and normalize slashes to FileBackend-style
+ // 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() {
} catch ( UnexpectedValueException $e ) {
$this->iter = null;
}
+ ++$this->pos;
}
public function rewind() {
+ $this->pos = 0;
try {
$this->iter->rewind();
} catch ( UnexpectedValueException $e ) {