9 * Class for a file system (FS) based file backend.
11 * All "containers" each map to a directory under the backend's base directory.
12 * For backwards-compatibility, some container paths can be set to custom paths.
13 * The wiki ID will not be used in any custom paths, so this should be avoided.
14 * Sharding can be accomplished by using FileRepo-style hash paths.
16 * Status messages should avoid mentioning the internal FS paths.
17 * Likewise, error suppression should be used to avoid path disclosure.
19 * @ingroup FileBackend
21 class FSFileBackend
extends FileBackend
{
22 protected $basePath; // string; directory holding the container directories
23 /** @var Array Map of container names to root paths */
24 protected $containerPaths = array(); // for custom container paths
25 protected $fileMode; // integer; file permission mode
28 * @see FileBackend::__construct()
29 * Additional $config params include:
30 * basePath : File system directory that holds containers.
31 * containerPaths : Map of container names to custom file system directories.
32 * This should only be used for backwards-compatibility.
33 * fileMode : Octal UNIX file permissions to use on files stored.
35 public function __construct( array $config ) {
36 parent
::__construct( $config );
37 if ( isset( $config['basePath'] ) ) {
38 if ( substr( $this->basePath
, -1 ) === '/' ) {
39 $this->basePath
= substr( $this->basePath
, 0, -1 ); // remove trailing slash
42 $this->basePath
= null; // none; containers must have explicit paths
44 $this->containerPaths
= (array)$config['containerPaths'];
45 foreach ( $this->containerPaths
as &$path ) {
46 if ( substr( $path, -1 ) === '/' ) {
47 $path = substr( $path, 0, -1 ); // remove trailing slash
50 $this->fileMode
= isset( $config['fileMode'] )
56 * @see FileBackend::resolveContainerPath()
58 protected function resolveContainerPath( $container, $relStoragePath ) {
59 if ( isset( $this->containerPaths
[$container] ) ||
isset( $this->basePath
) ) {
60 return $relStoragePath; // container has a root directory
66 * Given the short (unresolved) and full (resolved) name of
67 * a container, return the file system path of the container.
69 * @param $shortCont string
70 * @param $fullCont string
73 protected function containerFSRoot( $shortCont, $fullCont ) {
74 if ( isset( $this->containerPaths
[$shortCont] ) ) {
75 return $this->containerPaths
[$shortCont];
76 } elseif ( isset( $this->basePath
) ) {
77 return "{$this->basePath}/{$fullCont}";
79 return null; // no container base path defined
83 * Get the absolute file system path for a storage path
85 * @param $storagePath string Storage path
88 protected function resolveToFSPath( $storagePath ) {
89 list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
90 if ( $relPath === null ) {
91 return null; // invalid
93 list( $b, $shortCont, $r ) = FileBackend
::splitStoragePath( $storagePath );
94 $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
95 if ( $relPath != '' ) {
96 $fsPath .= "/{$relPath}";
102 * @see FileBackend::doStoreInternal()
104 protected function doStoreInternal( array $params ) {
105 $status = Status
::newGood();
107 $dest = $this->resolveToFSPath( $params['dst'] );
108 if ( $dest === null ) {
109 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
113 if ( file_exists( $dest ) ) {
114 if ( !empty( $params['overwriteDest'] ) ) {
115 wfSuppressWarnings();
116 $ok = unlink( $dest );
119 $status->fatal( 'backend-fail-delete', $params['dst'] );
123 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
127 if ( !wfMkdirParents( dirname( $dest ) ) ) {
128 $status->fatal( 'directorycreateerror', $params['dst'] );
133 wfSuppressWarnings();
134 $ok = copy( $params['src'], $dest );
137 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
141 $this->chmod( $dest );
147 * @see FileBackend::doCopyInternal()
149 protected function doCopyInternal( array $params ) {
150 $status = Status
::newGood();
152 $source = $this->resolveToFSPath( $params['src'] );
153 if ( $source === null ) {
154 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
158 $dest = $this->resolveToFSPath( $params['dst'] );
159 if ( $dest === null ) {
160 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
164 if ( file_exists( $dest ) ) {
165 if ( !empty( $params['overwriteDest'] ) ) {
166 wfSuppressWarnings();
167 $ok = unlink( $dest );
170 $status->fatal( 'backend-fail-delete', $params['dst'] );
174 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
178 if ( !wfMkdirParents( dirname( $dest ) ) ) {
179 $status->fatal( 'directorycreateerror', $params['dst'] );
184 wfSuppressWarnings();
185 $ok = copy( $source, $dest );
188 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
192 $this->chmod( $dest );
198 * @see FileBackend::doMoveInternal()
200 protected function doMoveInternal( array $params ) {
201 $status = Status
::newGood();
203 $source = $this->resolveToFSPath( $params['src'] );
204 if ( $source === null ) {
205 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
209 $dest = $this->resolveToFSPath( $params['dst'] );
210 if ( $dest === null ) {
211 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
215 if ( file_exists( $dest ) ) {
216 if ( !empty( $params['overwriteDest'] ) ) {
217 // Windows does not support moving over existing files
218 if ( wfIsWindows() ) {
219 wfSuppressWarnings();
220 $ok = unlink( $dest );
223 $status->fatal( 'backend-fail-delete', $params['dst'] );
228 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
232 if ( !wfMkdirParents( dirname( $dest ) ) ) {
233 $status->fatal( 'directorycreateerror', $params['dst'] );
238 wfSuppressWarnings();
239 $ok = rename( $source, $dest );
240 clearstatcache(); // file no longer at source
243 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
251 * @see FileBackend::doDeleteInternal()
253 protected function doDeleteInternal( array $params ) {
254 $status = Status
::newGood();
256 $source = $this->resolveToFSPath( $params['src'] );
257 if ( $source === null ) {
258 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
262 if ( !is_file( $source ) ) {
263 if ( empty( $params['ignoreMissingSource'] ) ) {
264 $status->fatal( 'backend-fail-delete', $params['src'] );
266 return $status; // do nothing; either OK or bad status
269 wfSuppressWarnings();
270 $ok = unlink( $source );
273 $status->fatal( 'backend-fail-delete', $params['src'] );
281 * @see FileBackend::doCreateInternal()
283 protected function doCreateInternal( array $params ) {
284 $status = Status
::newGood();
286 $dest = $this->resolveToFSPath( $params['dst'] );
287 if ( $dest === null ) {
288 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
292 if ( file_exists( $dest ) ) {
293 if ( !empty( $params['overwriteDest'] ) ) {
294 wfSuppressWarnings();
295 $ok = unlink( $dest );
298 $status->fatal( 'backend-fail-delete', $params['dst'] );
302 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
306 if ( !wfMkdirParents( dirname( $dest ) ) ) {
307 $status->fatal( 'directorycreateerror', $params['dst'] );
312 wfSuppressWarnings();
313 $ok = file_put_contents( $dest, $params['content'] );
316 $status->fatal( 'backend-fail-create', $params['dst'] );
320 $this->chmod( $dest );
326 * @see FileBackend::doPrepareInternal()
328 protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
329 $status = Status
::newGood();
330 list( $b, $shortCont, $r ) = FileBackend
::splitStoragePath( $params['dir'] );
331 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
332 $dir = ( $dirRel != '' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
333 if ( !wfMkdirParents( $dir ) ) {
334 $status->fatal( 'directorycreateerror', $params['dir'] );
335 } elseif ( !is_writable( $dir ) ) {
336 $status->fatal( 'directoryreadonlyerror', $params['dir'] );
337 } elseif ( !is_readable( $dir ) ) {
338 $status->fatal( 'directorynotreadableerror', $params['dir'] );
344 * @see FileBackend::doSecureInternal()
346 protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
347 $status = Status
::newGood();
348 list( $b, $shortCont, $r ) = FileBackend
::splitStoragePath( $params['dir'] );
349 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
350 $dir = ( $dirRel != '' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
351 if ( !wfMkdirParents( $dir ) ) {
352 $status->fatal( 'directorycreateerror', $params['dir'] );
355 // Seed new directories with a blank index.html, to prevent crawling...
356 if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
357 wfSuppressWarnings();
358 $ok = file_put_contents( "{$dir}/index.html", '' );
361 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
365 // Add a .htaccess file to the root of the container...
366 if ( !empty( $params['noAccess'] ) ) {
367 $dirRoot = $this->resolveToFSPath( $params['dir'], '' );
368 if ( !file_exists( "{$dirRoot}/.htaccess" ) ) {
369 wfSuppressWarnings();
370 $ok = file_put_contents( "{$dirRoot}/.htaccess", "Deny from all\n" );
373 $storeDir = "mwstore://{$this->name}/{$shortCont}";
374 $status->fatal( 'backend-fail-create', "$storeDir/.htaccess" );
383 * @see FileBackend::doCleanInternal()
385 protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
386 $status = Status
::newGood();
387 list( $b, $shortCont, $r ) = FileBackend
::splitStoragePath( $params['dir'] );
388 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
389 $dir = ( $dirRel != '' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
390 wfSuppressWarnings();
391 if ( is_dir( $dir ) ) {
392 rmdir( $dir ); // remove directory if empty
399 * @see FileBackend::doFileExists()
401 protected function doGetFileStat( array $params ) {
402 $source = $this->resolveToFSPath( $params['src'] );
403 if ( $source === null ) {
404 return false; // invalid storage path
407 wfSuppressWarnings();
408 $stat = is_file( $source ) ?
stat( $source ) : false; // regular files only
413 'mtime' => wfTimestamp( TS_MW
, $stat['mtime'] ),
414 'size' => $stat['size']
422 * @see FileBackend::getFileListInternal()
424 public function getFileListInternal( $fullCont, $dirRel, array $params ) {
425 list( $b, $shortCont, $r ) = FileBackend
::splitStoragePath( $params['dir'] );
426 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
427 $dir = ( $dirRel != '' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
428 wfSuppressWarnings();
429 $exists = is_dir( $dir );
432 return array(); // nothing under this dir
434 wfSuppressWarnings();
435 $readable = is_readable( $dir );
438 return null; // bad permissions?
440 return new FSFileIterator( $dir );
444 * @see FileBackend::getLocalReference()
446 public function getLocalReference( array $params ) {
447 $source = $this->resolveToFSPath( $params['src'] );
448 if ( $source === null ) {
451 return new FSFile( $source );
455 * @see FileBackend::getLocalCopy()
457 public function getLocalCopy( array $params ) {
458 $source = $this->resolveToFSPath( $params['src'] );
459 if ( $source === null ) {
463 // Create a new temporary file with the same extension...
464 $ext = FileBackend
::extensionFromPath( $params['src'] );
465 $tmpFile = TempFSFile
::factory( wfBaseName( $source ) . '_', $ext );
469 $tmpPath = $tmpFile->getPath();
471 // Copy the source file over the temp file
472 wfSuppressWarnings();
473 $ok = copy( $source, $tmpPath );
479 $this->chmod( $tmpPath );
485 * Chmod a file, suppressing the warnings
487 * @param $path string Absolute file system path
488 * @return bool Success
490 protected function chmod( $path ) {
491 wfSuppressWarnings();
492 $ok = chmod( $path, $this->fileMode
);
500 * Wrapper around RecursiveDirectoryIterator that catches
501 * exception or does any custom behavoir that we may want.
503 * @ingroup FileBackend
505 class FSFileIterator
implements Iterator
{
506 /** @var RecursiveIteratorIterator */
508 protected $suffixStart; // integer
511 * Get an FSFileIterator from a file system directory
515 public function __construct( $dir ) {
516 $this->suffixStart
= strlen( realpath( $dir ) ) +
1; // size of "path/to/dir/"
518 $flags = FilesystemIterator
::CURRENT_AS_FILEINFO | FilesystemIterator
::SKIP_DOTS
;
519 $this->iter
= new RecursiveIteratorIterator(
520 new RecursiveDirectoryIterator( $dir, $flags ) );
521 } catch ( UnexpectedValueException
$e ) {
522 $this->iter
= null; // bad permissions? deleted?
526 public function current() {
527 // Return only the relative path and normalize slashes to FileBackend-style
528 // Make sure to use the realpath since the suffix is based upon that
529 return str_replace( '\\', '/',
530 substr( realpath( $this->iter
->current() ), $this->suffixStart
) );
533 public function key() {
534 return $this->iter
->key();
537 public function next() {
540 } catch ( UnexpectedValueException
$e ) {
545 public function rewind() {
547 $this->iter
->rewind();
548 } catch ( UnexpectedValueException
$e ) {
553 public function valid() {
554 return $this->iter
&& $this->iter
->valid();