*/
/**
- * 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 ) === '/' ) {
}
/**
- * @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();
$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->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();
$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->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();
$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->resolveStoragePathReal( $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->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();
$status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
return $status;
}
- } else {
- if ( !wfMkdirParents( dirname( $dest ) ) ) {
- $status->fatal( 'directorycreateerror', $params['dst'] );
- return $status;
- }
}
wfSuppressWarnings();
}
/**
- * @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'] );
}
/**
- * @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();
}
}
// 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
}
/**
- * @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();
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;
}
}
/**
- * @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;
}
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?
}
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() {
} catch ( UnexpectedValueException $e ) {
$this->iter = null;
}
+ ++$this->pos;
}
public function rewind() {
+ $this->pos = 0;
try {
$this->iter->rewind();
} catch ( UnexpectedValueException $e ) {