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.
15 * Having directories with thousands of files will diminish performance.
16 * Sharding can be accomplished by using FileRepo-style hash paths.
18 * Status messages should avoid mentioning the internal FS paths.
19 * Likewise, error suppression should be used to avoid path disclosure.
21 * @ingroup FileBackend
23 class FSFileBackend
extends FileBackend
{
24 protected $basePath; // string; directory holding the container directories
25 /** @var Array Map of container names to root paths */
26 protected $containerPaths = array(); // for custom container paths
27 protected $fileMode; // integer; file permission mode
30 * @see FileBackend::__construct()
31 * Additional $config params include:
32 * basePath : File system directory that holds containers.
33 * containerPaths : Map of container names to custom file system directories.
34 * This should only be used for backwards-compatibility.
35 * fileMode : Octal UNIX file permissions to use on files stored.
37 public function __construct( array $config ) {
38 parent
::__construct( $config );
39 if ( isset( $config['basePath'] ) ) {
40 if ( substr( $this->basePath
, -1 ) === '/' ) {
41 $this->basePath
= substr( $this->basePath
, 0, -1 ); // remove trailing slash
44 $this->basePath
= null; // none; containers must have explicit paths
46 $this->containerPaths
= (array)$config['containerPaths'];
47 foreach ( $this->containerPaths
as &$path ) {
48 if ( substr( $path, -1 ) === '/' ) {
49 $path = substr( $path, 0, -1 ); // remove trailing slash
52 $this->fileMode
= isset( $config['fileMode'] )
58 * @see FileBackend::resolveContainerPath()
60 protected function resolveContainerPath( $container, $relStoragePath ) {
61 if ( isset( $this->containerPaths
[$container] ) ||
isset( $this->basePath
) ) {
62 return $relStoragePath; // container has a root directory
68 * Given the short (unresolved) and full (resolved) name of
69 * a container, return the file system path of the container.
71 * @param $shortCont string
72 * @param $fullCont string
75 protected function containerFSRoot( $shortCont, $fullCont ) {
76 if ( isset( $this->containerPaths
[$shortCont] ) ) {
77 return $this->containerPaths
[$shortCont];
78 } elseif ( isset( $this->basePath
) ) {
79 return "{$this->basePath}/{$fullCont}";
81 return null; // no container base path defined
85 * Get the absolute file system path for a storage path
87 * @param $storagePath string Storage path
90 protected function resolveToFSPath( $storagePath ) {
91 list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath );
92 if ( $relPath === null ) {
93 return null; // invalid
95 list( $b, $shortCont, $r ) = FileBackend
::splitStoragePath( $storagePath );
96 $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
97 if ( $relPath != '' ) {
98 $fsPath .= "/{$relPath}";
104 * @see FileBackend::doStoreInternal()
106 protected function doStoreInternal( array $params ) {
107 $status = Status
::newGood();
109 $dest = $this->resolveToFSPath( $params['dst'] );
110 if ( $dest === null ) {
111 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
115 if ( file_exists( $dest ) ) {
116 if ( !empty( $params['overwriteDest'] ) ) {
117 wfSuppressWarnings();
118 $ok = unlink( $dest );
121 $status->fatal( 'backend-fail-delete', $params['dst'] );
125 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
129 if ( !wfMkdirParents( dirname( $dest ) ) ) {
130 $status->fatal( 'directorycreateerror', $params['dst'] );
135 wfSuppressWarnings();
136 $ok = copy( $params['src'], $dest );
139 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
143 $this->chmod( $dest );
149 * @see FileBackend::doCopyInternal()
151 protected function doCopyInternal( array $params ) {
152 $status = Status
::newGood();
154 $source = $this->resolveToFSPath( $params['src'] );
155 if ( $source === null ) {
156 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
160 $dest = $this->resolveToFSPath( $params['dst'] );
161 if ( $dest === null ) {
162 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
166 if ( file_exists( $dest ) ) {
167 if ( !empty( $params['overwriteDest'] ) ) {
168 wfSuppressWarnings();
169 $ok = unlink( $dest );
172 $status->fatal( 'backend-fail-delete', $params['dst'] );
176 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
180 if ( !wfMkdirParents( dirname( $dest ) ) ) {
181 $status->fatal( 'directorycreateerror', $params['dst'] );
186 wfSuppressWarnings();
187 $ok = copy( $source, $dest );
190 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
194 $this->chmod( $dest );
200 * @see FileBackend::doMoveInternal()
202 protected function doMoveInternal( array $params ) {
203 $status = Status
::newGood();
205 $source = $this->resolveToFSPath( $params['src'] );
206 if ( $source === null ) {
207 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
211 $dest = $this->resolveToFSPath( $params['dst'] );
212 if ( $dest === null ) {
213 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
217 if ( file_exists( $dest ) ) {
218 if ( !empty( $params['overwriteDest'] ) ) {
219 // Windows does not support moving over existing files
220 if ( wfIsWindows() ) {
221 wfSuppressWarnings();
222 $ok = unlink( $dest );
225 $status->fatal( 'backend-fail-delete', $params['dst'] );
230 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
234 if ( !wfMkdirParents( dirname( $dest ) ) ) {
235 $status->fatal( 'directorycreateerror', $params['dst'] );
240 wfSuppressWarnings();
241 $ok = rename( $source, $dest );
242 clearstatcache(); // file no longer at source
245 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
253 * @see FileBackend::doDeleteInternal()
255 protected function doDeleteInternal( array $params ) {
256 $status = Status
::newGood();
258 $source = $this->resolveToFSPath( $params['src'] );
259 if ( $source === null ) {
260 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
264 if ( !is_file( $source ) ) {
265 if ( empty( $params['ignoreMissingSource'] ) ) {
266 $status->fatal( 'backend-fail-delete', $params['src'] );
268 return $status; // do nothing; either OK or bad status
271 wfSuppressWarnings();
272 $ok = unlink( $source );
275 $status->fatal( 'backend-fail-delete', $params['src'] );
283 * @see FileBackend::doCreateInternal()
285 protected function doCreateInternal( array $params ) {
286 $status = Status
::newGood();
288 $dest = $this->resolveToFSPath( $params['dst'] );
289 if ( $dest === null ) {
290 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
294 if ( file_exists( $dest ) ) {
295 if ( !empty( $params['overwriteDest'] ) ) {
296 wfSuppressWarnings();
297 $ok = unlink( $dest );
300 $status->fatal( 'backend-fail-delete', $params['dst'] );
304 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
308 if ( !wfMkdirParents( dirname( $dest ) ) ) {
309 $status->fatal( 'directorycreateerror', $params['dst'] );
314 wfSuppressWarnings();
315 $ok = file_put_contents( $dest, $params['content'] );
318 $status->fatal( 'backend-fail-create', $params['dst'] );
322 $this->chmod( $dest );
328 * @see FileBackend::doPrepareInternal()
330 protected function doPrepareInternal( $fullCont, $dirRel, array $params ) {
331 $status = Status
::newGood();
332 list( $b, $shortCont, $r ) = FileBackend
::splitStoragePath( $params['dir'] );
333 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
334 $dir = ( $dirRel != '' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
335 if ( !wfMkdirParents( $dir ) ) {
336 $status->fatal( 'directorycreateerror', $params['dir'] );
337 } elseif ( !is_writable( $dir ) ) {
338 $status->fatal( 'directoryreadonlyerror', $params['dir'] );
339 } elseif ( !is_readable( $dir ) ) {
340 $status->fatal( 'directorynotreadableerror', $params['dir'] );
346 * @see FileBackend::doSecureInternal()
348 protected function doSecureInternal( $fullCont, $dirRel, array $params ) {
349 $status = Status
::newGood();
350 list( $b, $shortCont, $r ) = FileBackend
::splitStoragePath( $params['dir'] );
351 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
352 $dir = ( $dirRel != '' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
353 if ( !wfMkdirParents( $dir ) ) {
354 $status->fatal( 'directorycreateerror', $params['dir'] );
357 // Seed new directories with a blank index.html, to prevent crawling...
358 if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
359 wfSuppressWarnings();
360 $ok = file_put_contents( "{$dir}/index.html", '' );
363 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
367 // Add a .htaccess file to the root of the container...
368 if ( !empty( $params['noAccess'] ) ) {
369 if ( !file_exists( "{$contRoot}/.htaccess" ) ) {
370 wfSuppressWarnings();
371 $ok = file_put_contents( "{$dirRoot}/.htaccess", "Deny from all\n" );
374 $storeDir = "mwstore://{$this->name}/{$shortCont}";
375 $status->fatal( 'backend-fail-create', "{$storeDir}/.htaccess" );
384 * @see FileBackend::doCleanInternal()
386 protected function doCleanInternal( $fullCont, $dirRel, array $params ) {
387 $status = Status
::newGood();
388 list( $b, $shortCont, $r ) = FileBackend
::splitStoragePath( $params['dir'] );
389 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
390 $dir = ( $dirRel != '' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
391 wfSuppressWarnings();
392 if ( is_dir( $dir ) ) {
393 rmdir( $dir ); // remove directory if empty
400 * @see FileBackend::doFileExists()
402 protected function doGetFileStat( array $params ) {
403 $source = $this->resolveToFSPath( $params['src'] );
404 if ( $source === null ) {
405 return false; // invalid storage path
408 $this->trapWarnings();
409 $stat = is_file( $source ) ?
stat( $source ) : false; // regular files only
410 $hadError = $this->untrapWarnings();
414 'mtime' => wfTimestamp( TS_MW
, $stat['mtime'] ),
415 'size' => $stat['size']
417 } elseif ( !$hadError ) {
418 return false; // file does not exist
420 return null; // failure
425 * @see FileBackend::getFileListInternal()
427 public function getFileListInternal( $fullCont, $dirRel, array $params ) {
428 list( $b, $shortCont, $r ) = FileBackend
::splitStoragePath( $params['dir'] );
429 $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid
430 $dir = ( $dirRel != '' ) ?
"{$contRoot}/{$dirRel}" : $contRoot;
431 wfSuppressWarnings();
432 $exists = is_dir( $dir );
435 return array(); // nothing under this dir
437 wfSuppressWarnings();
438 $readable = is_readable( $dir );
441 return null; // bad permissions?
443 return new FSFileIterator( $dir );
447 * @see FileBackend::getLocalReference()
449 public function getLocalReference( array $params ) {
450 $source = $this->resolveToFSPath( $params['src'] );
451 if ( $source === null ) {
454 return new FSFile( $source );
458 * @see FileBackend::getLocalCopy()
460 public function getLocalCopy( array $params ) {
461 $source = $this->resolveToFSPath( $params['src'] );
462 if ( $source === null ) {
466 // Create a new temporary file with the same extension...
467 $ext = FileBackend
::extensionFromPath( $params['src'] );
468 $tmpFile = TempFSFile
::factory( wfBaseName( $source ) . '_', $ext );
472 $tmpPath = $tmpFile->getPath();
474 // Copy the source file over the temp file
475 wfSuppressWarnings();
476 $ok = copy( $source, $tmpPath );
482 $this->chmod( $tmpPath );
488 * Chmod a file, suppressing the warnings
490 * @param $path string Absolute file system path
491 * @return bool Success
493 protected function chmod( $path ) {
494 wfSuppressWarnings();
495 $ok = chmod( $path, $this->fileMode
);
502 * Suppress E_WARNING errors and track whether any happen
506 protected function trapWarnings() {
507 $this->hadWarningErrors
[] = false; // push to stack
508 set_error_handler( array( $this, 'handleWarning' ), E_WARNING
);
512 * Unsuppress E_WARNING errors and return true if any happened
516 protected function untrapWarnings() {
517 restore_error_handler(); // restore previous handler
518 return array_pop( $this->hadWarningErrors
); // pop from stack
521 private function handleWarning() {
522 $this->hadWarningErrors
[count( $this->hadWarningErrors
) - 1] = true;
523 return true; // suppress from PHP handler
528 * Wrapper around RecursiveDirectoryIterator that catches
529 * exception or does any custom behavoir that we may want.
531 * @ingroup FileBackend
533 class FSFileIterator
implements Iterator
{
534 /** @var RecursiveIteratorIterator */
536 protected $suffixStart; // integer
539 * Get an FSFileIterator from a file system directory
543 public function __construct( $dir ) {
544 $dir = realpath( $dir ); // normalize
545 $this->suffixStart
= strlen( $dir ) +
1; // size of "path/to/dir/"
547 $flags = FilesystemIterator
::CURRENT_AS_FILEINFO | FilesystemIterator
::SKIP_DOTS
;
548 $this->iter
= new RecursiveIteratorIterator(
549 new RecursiveDirectoryIterator( $dir, $flags ) );
550 } catch ( UnexpectedValueException
$e ) {
551 $this->iter
= null; // bad permissions? deleted?
555 public function current() {
556 // Return only the relative path and normalize slashes to FileBackend-style
557 // Make sure to use the realpath since the suffix is based upon that
558 return str_replace( '\\', '/',
559 substr( realpath( $this->iter
->current() ), $this->suffixStart
) );
562 public function key() {
563 return $this->iter
->key();
566 public function next() {
569 } catch ( UnexpectedValueException
$e ) {
574 public function rewind() {
576 $this->iter
->rewind();
577 } catch ( UnexpectedValueException
$e ) {
582 public function valid() {
583 return $this->iter
&& $this->iter
->valid();