9 * Class for a file system based file backend.
10 * Status messages should avoid mentioning the internal FS paths.
11 * Likewise, error suppression should be used to avoid path disclosure.
13 * @ingroup FileBackend
15 class FSFileBackend
extends FileBackend
{
16 /** @var Array Map of container names to paths */
17 protected $containerPaths = array();
18 protected $fileMode; // file permission mode
21 * @see FileBackend::__construct()
22 * Additional $config params include:
23 * containerPaths : Map of container names to absolute file system paths
24 * fileMode : Octal UNIX file permissions to use on files stored
26 function __construct( array $config ) {
27 parent
::__construct( $config );
28 $this->containerPaths
= (array)$config['containerPaths'];
29 foreach ( $this->containerPaths
as $container => &$path ) {
30 if ( substr( $path, -1 ) === '/' ) {
31 $path = substr( $path, 0, -1 ); // remove trailing slash
34 $this->fileMode
= isset( $config['fileMode'] )
40 * @see FileBackend::resolveContainerPath()
42 protected function resolveContainerPath( $container, $relStoragePath ) {
43 // Get absolute path given the container base dir
44 if ( isset( $this->containerPaths
[$container] ) ) {
45 return $this->containerPaths
[$container] . "/{$relStoragePath}";
51 * @see FileBackend::doStore()
53 protected function doStore( array $params ) {
54 $status = Status
::newGood();
56 list( $c, $dest ) = $this->resolveStoragePath( $params['dst'] );
57 if ( $dest === null ) {
58 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
61 if ( file_exists( $dest ) ) {
62 if ( !empty( $params['overwriteDest'] ) ) {
64 $ok = unlink( $dest );
67 $status->fatal( 'backend-fail-delete', $params['dst'] );
71 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
75 if ( !wfMkdirParents( dirname( $dest ) ) ) {
76 $status->fatal( 'directorycreateerror', $params['dst'] );
82 $ok = copy( $params['src'], $dest );
85 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
89 $this->chmod( $dest );
95 * @see FileBackend::doCopy()
97 protected function doCopy( array $params ) {
98 $status = Status
::newGood();
100 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
101 if ( $source === null ) {
102 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
106 list( $c, $dest ) = $this->resolveStoragePath( $params['dst'] );
107 if ( $dest === null ) {
108 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
112 if ( file_exists( $dest ) ) {
113 if ( !empty( $params['overwriteDest'] ) ) {
114 wfSuppressWarnings();
115 $ok = unlink( $dest );
118 $status->fatal( 'backend-fail-delete', $params['dst'] );
122 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
126 if ( !wfMkdirParents( dirname( $dest ) ) ) {
127 $status->fatal( 'directorycreateerror', $params['dst'] );
132 wfSuppressWarnings();
133 $ok = copy( $source, $dest );
136 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
140 $this->chmod( $dest );
146 * @see FileBackend::doMove()
148 protected function doMove( array $params ) {
149 $status = Status
::newGood();
151 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
152 if ( $source === null ) {
153 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
156 list( $c, $dest ) = $this->resolveStoragePath( $params['dst'] );
157 if ( $dest === null ) {
158 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
162 if ( file_exists( $dest ) ) {
163 if ( !empty( $params['overwriteDest'] ) ) {
164 // Windows does not support moving over existing files
165 if ( wfIsWindows() ) {
166 wfSuppressWarnings();
167 $ok = unlink( $dest );
170 $status->fatal( 'backend-fail-delete', $params['dst'] );
175 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
179 if ( !wfMkdirParents( dirname( $dest ) ) ) {
180 $status->fatal( 'directorycreateerror', $params['dst'] );
185 wfSuppressWarnings();
186 $ok = rename( $source, $dest );
187 clearstatcache(); // file no longer at source
190 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
198 * @see FileBackend::doDelete()
200 protected function doDelete( array $params ) {
201 $status = Status
::newGood();
203 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
204 if ( $source === null ) {
205 $status->fatal( 'backend-fail-invalidpath', $params['src'] );
209 if ( !is_file( $source ) ) {
210 if ( empty( $params['ignoreMissingSource'] ) ) {
211 $status->fatal( 'backend-fail-delete', $params['src'] );
213 return $status; // do nothing; either OK or bad status
216 wfSuppressWarnings();
217 $ok = unlink( $source );
220 $status->fatal( 'backend-fail-delete', $params['src'] );
228 * @see FileBackend::doConcatenate()
230 protected function doConcatenate( array $params ) {
231 $status = Status
::newGood();
233 list( $c, $dest ) = $this->resolveStoragePath( $params['dst'] );
234 if ( $dest === null ) {
235 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
239 // Check if the destination file exists and we can't handle that
240 $destExists = file_exists( $dest );
241 if ( $destExists && empty( $params['overwriteDest'] ) ) {
242 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
246 // Create a new temporary file...
247 wfSuppressWarnings();
248 $tmpPath = tempnam( wfTempDir(), 'concatenate' );
250 if ( $tmpPath === false ) {
251 $status->fatal( 'backend-fail-createtemp' );
255 // Build up that file using the source chunks (in order)...
256 wfSuppressWarnings();
257 $tmpHandle = fopen( $tmpPath, 'a' );
259 if ( $tmpHandle === false ) {
260 $status->fatal( 'backend-fail-opentemp', $tmpPath );
263 foreach ( $params['srcs'] as $virtualSource ) {
264 list( $c, $source ) = $this->resolveStoragePath( $virtualSource );
265 if ( $source === null ) {
266 fclose( $tmpHandle );
267 $status->fatal( 'backend-fail-invalidpath', $virtualSource );
270 // Load chunk into memory (it should be a small file)
271 $sourceHandle = fopen( $source, 'r' );
272 if ( $sourceHandle === false ) {
273 fclose( $tmpHandle );
274 $status->fatal( 'backend-fail-read', $virtualSource );
277 // Append chunk to file (pass chunk size to avoid magic quotes)
278 if ( !stream_copy_to_stream( $sourceHandle, $tmpHandle ) ) {
279 fclose( $sourceHandle );
280 fclose( $tmpHandle );
281 $status->fatal( 'backend-fail-writetemp', $tmpPath );
284 fclose( $sourceHandle );
286 wfSuppressWarnings();
287 if ( !fclose( $tmpHandle ) ) {
288 $status->fatal( 'backend-fail-closetemp', $tmpPath );
293 // Handle overwrite behavior of file destination if applicable.
294 // Note that we already checked if no overwrite params were set above.
296 // Windows does not support moving over existing files
297 if ( wfIsWindows() ) {
298 wfSuppressWarnings();
299 $ok = unlink( $dest );
302 $status->fatal( 'backend-fail-delete', $params['dst'] );
307 // Make sure destination directory exists
308 if ( !wfMkdirParents( dirname( $dest ) ) ) {
309 $status->fatal( 'directorycreateerror', $params['dst'] );
314 // Rename the temporary file to the destination path
315 wfSuppressWarnings();
316 $ok = rename( $tmpPath, $dest );
319 $status->fatal( 'backend-fail-move', $tmpPath, $params['dst'] );
323 $this->chmod( $dest );
329 * @see FileBackend::doCreate()
331 protected function doCreate( array $params ) {
332 $status = Status
::newGood();
334 list( $c, $dest ) = $this->resolveStoragePath( $params['dst'] );
335 if ( $dest === null ) {
336 $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
340 if ( file_exists( $dest ) ) {
341 if ( !empty( $params['overwriteDest'] ) ) {
342 wfSuppressWarnings();
343 $ok = unlink( $dest );
346 $status->fatal( 'backend-fail-delete', $params['dst'] );
350 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
354 if ( !wfMkdirParents( dirname( $dest ) ) ) {
355 $status->fatal( 'directorycreateerror', $params['dst'] );
360 wfSuppressWarnings();
361 $ok = file_put_contents( $dest, $params['content'] );
364 $status->fatal( 'backend-fail-create', $params['dst'] );
368 $this->chmod( $dest );
374 * @see FileBackend::prepare()
376 function prepare( array $params ) {
377 $status = Status
::newGood();
378 list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
379 if ( $dir === null ) {
380 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
381 return $status; // invalid storage path
383 if ( !wfMkdirParents( $dir ) ) {
384 $status->fatal( 'directorycreateerror', $params['dir'] );
386 } elseif ( !is_writable( $dir ) ) {
387 $status->fatal( 'directoryreadonlyerror', $params['dir'] );
389 } elseif ( !is_readable( $dir ) ) {
390 $status->fatal( 'directorynotreadableerror', $params['dir'] );
397 * @see FileBackend::secure()
399 function secure( array $params ) {
400 $status = Status
::newGood();
401 list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
402 if ( $dir === null ) {
403 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
404 return $status; // invalid storage path
406 if ( !wfMkdirParents( $dir ) ) {
407 $status->fatal( 'directorycreateerror', $params['dir'] );
410 // Add a .htaccess file to the root of the deleted zone
411 if ( !empty( $params['noAccess'] ) && !file_exists( "{$dir}/.htaccess" ) ) {
412 wfSuppressWarnings();
413 $ok = file_put_contents( "{$dir}/.htaccess", "Deny from all\n" );
416 $status->fatal( 'backend-fail-create', $params['dir'] . '/.htaccess' );
420 // Seed new directories with a blank index.html, to prevent crawling
421 if ( !empty( $params['noListing'] ) && !file_exists( "{$dir}/index.html" ) ) {
422 wfSuppressWarnings();
423 $ok = file_put_contents( "{$dir}/index.html", '' );
426 $status->fatal( 'backend-fail-create', $params['dir'] . '/index.html' );
434 * @see FileBackend::clean()
436 function clean( array $params ) {
437 $status = Status
::newGood();
438 list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
439 if ( $dir === null ) {
440 $status->fatal( 'backend-fail-invalidpath', $params['dir'] );
441 return $status; // invalid storage path
443 wfSuppressWarnings();
444 if ( is_dir( $dir ) ) {
445 rmdir( $dir ); // remove directory if empty
452 * @see FileBackend::fileExists()
454 function fileExists( array $params ) {
455 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
456 if ( $source === null ) {
457 return false; // invalid storage path
459 wfSuppressWarnings();
460 $exists = is_file( $source );
466 * @see FileBackend::getFileTimestamp()
468 function getFileTimestamp( array $params ) {
469 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
470 if ( $source === null ) {
471 return false; // invalid storage path
473 $fsFile = new FSFile( $source );
474 return $fsFile->getTimestamp();
478 * @see FileBackend::getFileList()
480 function getFileList( array $params ) {
481 list( $c, $dir ) = $this->resolveStoragePath( $params['dir'] );
482 if ( $dir === null ) { // invalid storage path
485 wfSuppressWarnings();
486 $exists = is_dir( $dir );
489 return array(); // nothing under this dir
491 wfSuppressWarnings();
492 $readable = is_readable( $dir );
495 return null; // bad permissions?
497 return new FSFileIterator( $dir );
501 * @see FileBackend::getLocalReference()
503 function getLocalReference( array $params ) {
504 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
505 if ( $source === null ) {
508 return new FSFile( $source );
512 * @see FileBackend::getLocalCopy()
514 function getLocalCopy( array $params ) {
515 list( $c, $source ) = $this->resolveStoragePath( $params['src'] );
516 if ( $source === null ) {
520 // Get source file extension
521 $i = strrpos( $source, '.' );
522 $ext = strtolower( $i ?
substr( $source, $i +
1 ) : '' );
523 // Create a new temporary file...
524 $tmpFile = TempFSFile
::factory( wfBaseName( $source ) . '_', $ext );
528 $tmpPath = $tmpFile->getPath();
530 // Copy the source file over the temp file
531 wfSuppressWarnings();
532 $ok = copy( $source, $tmpPath );
538 $this->chmod( $tmpPath );
544 * Chmod a file, suppressing the warnings
546 * @param $path string Absolute file system path
547 * @return bool Success
549 protected function chmod( $path ) {
550 wfSuppressWarnings();
551 $ok = chmod( $path, $this->fileMode
);
559 * Wrapper around RecursiveDirectoryIterator that catches
560 * exception or does any custom behavoir that we may want.
562 * @ingroup FileBackend
564 class FSFileIterator
implements Iterator
{
565 /** @var RecursiveIteratorIterator */
569 * Get an FSFileIterator from a file system directory
573 public function __construct( $dir ) {
575 $this->iter
= new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) );
576 } catch ( UnexpectedValueException
$e ) {
577 $this->iter
= null; // bad permissions? deleted?
581 public function current() {
582 return $this->iter
->current();
585 public function key() {
586 return $this->iter
->key();
589 public function next() {
592 } catch ( UnexpectedValueException
$e ) {
597 public function rewind() {
599 $this->iter
->rewind();
600 } catch ( UnexpectedValueException
$e ) {
605 public function valid() {
606 return $this->iter
&& $this->iter
->valid();