Index: trunk/phase3/tests/phpunit/includes/filerepo/StoreBatchTest.php |
— | — | @@ -1,6 +1,6 @@ |
2 | 2 | <?php |
3 | 3 | /** |
4 | | - * @group Filerepo |
| 4 | + * @group FileRepo |
5 | 5 | */ |
6 | 6 | class StoreBatchTest extends MediaWikiTestCase { |
7 | 7 | |
Index: trunk/phase3/tests/phpunit/includes/filerepo/FileBackendTest.php |
— | — | @@ -1,6 +1,9 @@ |
2 | 2 | <?php |
3 | 3 | |
4 | | -// @TODO: fix empty dir leakage |
| 4 | +/** |
| 5 | + * @group FileRepo |
| 6 | + * @TODO: fix empty dir leakage |
| 7 | + */ |
5 | 8 | class FileBackendTest extends MediaWikiTestCase { |
6 | 9 | private $backend, $multiBackend; |
7 | 10 | private $filesToPrune, $pathsToPrune; |
Index: trunk/phase3/includes/filerepo/backend/FSFileBackend.php |
— | — | @@ -6,10 +6,12 @@ |
7 | 7 | */ |
8 | 8 | |
9 | 9 | /** |
10 | | - * Class for a file system based file backend. |
11 | | - * Containers are just directories and container sharding is not supported. |
12 | | - * Also, for backwards-compatibility, the wiki ID prefix is not used. |
13 | | - * Users of this class should set wiki-specific container paths as needed. |
| 10 | + * Class for a file system (FS) based file backend. |
| 11 | + * |
| 12 | + * All "containers" each map to a directory under the backend's base directory. |
| 13 | + * For backwards-compatibility, some container paths can be set to custom paths. |
| 14 | + * The wiki ID will not be used in any custom paths, so this should be avoided. |
| 15 | + * Sharding can be accomplished by using FileRepo-style hash paths. |
14 | 16 | * |
15 | 17 | * Status messages should avoid mentioning the internal FS paths. |
16 | 18 | * Likewise, error suppression should be used to avoid path disclosure. |
— | — | @@ -17,18 +19,28 @@ |
18 | 20 | * @ingroup FileBackend |
19 | 21 | */ |
20 | 22 | class FSFileBackend extends FileBackend { |
21 | | - /** @var Array Map of container names to paths */ |
22 | | - protected $containerPaths = array(); |
23 | | - protected $fileMode; // file permission mode |
| 23 | + protected $basePath; // string; directory holding the container directories |
| 24 | + /** @var Array Map of container names to root paths */ |
| 25 | + protected $containerPaths = array(); // for custom container paths |
| 26 | + protected $fileMode; // integer; file permission mode |
24 | 27 | |
25 | 28 | /** |
26 | 29 | * @see FileBackend::__construct() |
27 | 30 | * Additional $config params include: |
28 | | - * containerPaths : Map of container names to absolute file system paths |
29 | | - * fileMode : Octal UNIX file permissions to use on files stored |
| 31 | + * basePath : File system directory that holds containers. |
| 32 | + * containerPaths : Map of container names to custom file system directories. |
| 33 | + * This should only be used for backwards-compatibility. |
| 34 | + * fileMode : Octal UNIX file permissions to use on files stored. |
30 | 35 | */ |
31 | 36 | public function __construct( array $config ) { |
32 | 37 | parent::__construct( $config ); |
| 38 | + if ( isset( $config['basePath'] ) ) { |
| 39 | + if ( substr( $this->basePath, -1 ) === '/' ) { |
| 40 | + $this->basePath = substr( $this->basePath, 0, -1 ); // remove trailing slash |
| 41 | + } |
| 42 | + } else { |
| 43 | + $this->basePath = null; // none; containers must have explicit paths |
| 44 | + } |
33 | 45 | $this->containerPaths = (array)$config['containerPaths']; |
34 | 46 | foreach ( $this->containerPaths as &$path ) { |
35 | 47 | if ( substr( $path, -1 ) === '/' ) { |
— | — | @@ -44,24 +56,60 @@ |
45 | 57 | * @see FileBackend::resolveContainerPath() |
46 | 58 | */ |
47 | 59 | protected function resolveContainerPath( $container, $relStoragePath ) { |
48 | | - // Get absolute path given the container base dir |
49 | | - if ( isset( $this->containerPaths[$container] ) ) { |
50 | | - return $this->containerPaths[$container] . "/{$relStoragePath}"; |
| 60 | + if ( isset( $this->containerPaths[$container] ) || isset( $this->basePath ) ) { |
| 61 | + return $relStoragePath; // container has a root directory |
51 | 62 | } |
52 | 63 | return null; |
53 | 64 | } |
54 | 65 | |
55 | 66 | /** |
| 67 | + * Given the short (unresolved) and full (resolved) name of |
| 68 | + * a container, return the file system path of the container. |
| 69 | + * |
| 70 | + * @param $shortCont string |
| 71 | + * @param $fullCont string |
| 72 | + * @return string|null |
| 73 | + */ |
| 74 | + protected function containerFSRoot( $shortCont, $fullCont ) { |
| 75 | + if ( isset( $this->containerPaths[$shortCont] ) ) { |
| 76 | + return $this->containerPaths[$shortCont]; |
| 77 | + } elseif ( isset( $this->basePath ) ) { |
| 78 | + return "{$this->basePath}/{$fullCont}"; |
| 79 | + } |
| 80 | + return null; // no container base path defined |
| 81 | + } |
| 82 | + |
| 83 | + /** |
| 84 | + * Get the absolute file system path for a storage path |
| 85 | + * |
| 86 | + * @param $storagePath string Storage path |
| 87 | + * @return string|null |
| 88 | + */ |
| 89 | + protected function resolveToFSPath( $storagePath ) { |
| 90 | + list( $fullCont, $relPath ) = $this->resolveStoragePathReal( $storagePath ); |
| 91 | + if ( $relPath === null ) { |
| 92 | + return null; // invalid |
| 93 | + } |
| 94 | + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $storagePath ); |
| 95 | + $fsPath = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid |
| 96 | + if ( $relPath != '' ) { |
| 97 | + $fsPath .= "/{$relPath}"; |
| 98 | + } |
| 99 | + return $fsPath; |
| 100 | + } |
| 101 | + |
| 102 | + /** |
56 | 103 | * @see FileBackend::doStoreInternal() |
57 | 104 | */ |
58 | 105 | protected function doStoreInternal( array $params ) { |
59 | 106 | $status = Status::newGood(); |
60 | 107 | |
61 | | - list( $c, $dest ) = $this->resolveStoragePathReal( $params['dst'] ); |
| 108 | + $dest = $this->resolveToFSPath( $params['dst'] ); |
62 | 109 | if ( $dest === null ) { |
63 | 110 | $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); |
64 | 111 | return $status; |
65 | 112 | } |
| 113 | + |
66 | 114 | if ( file_exists( $dest ) ) { |
67 | 115 | if ( !empty( $params['overwriteDest'] ) ) { |
68 | 116 | wfSuppressWarnings(); |
— | — | @@ -101,13 +149,13 @@ |
102 | 150 | protected function doCopyInternal( array $params ) { |
103 | 151 | $status = Status::newGood(); |
104 | 152 | |
105 | | - list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] ); |
| 153 | + $source = $this->resolveToFSPath( $params['src'] ); |
106 | 154 | if ( $source === null ) { |
107 | 155 | $status->fatal( 'backend-fail-invalidpath', $params['src'] ); |
108 | 156 | return $status; |
109 | 157 | } |
110 | 158 | |
111 | | - list( $c, $dest ) = $this->resolveStoragePathReal( $params['dst'] ); |
| 159 | + $dest = $this->resolveToFSPath( $params['dst'] ); |
112 | 160 | if ( $dest === null ) { |
113 | 161 | $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); |
114 | 162 | return $status; |
— | — | @@ -152,12 +200,13 @@ |
153 | 201 | protected function doMoveInternal( array $params ) { |
154 | 202 | $status = Status::newGood(); |
155 | 203 | |
156 | | - list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] ); |
| 204 | + $source = $this->resolveToFSPath( $params['src'] ); |
157 | 205 | if ( $source === null ) { |
158 | 206 | $status->fatal( 'backend-fail-invalidpath', $params['src'] ); |
159 | 207 | return $status; |
160 | 208 | } |
161 | | - list( $c, $dest ) = $this->resolveStoragePathReal( $params['dst'] ); |
| 209 | + |
| 210 | + $dest = $this->resolveToFSPath( $params['dst'] ); |
162 | 211 | if ( $dest === null ) { |
163 | 212 | $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); |
164 | 213 | return $status; |
— | — | @@ -204,7 +253,7 @@ |
205 | 254 | protected function doDeleteInternal( array $params ) { |
206 | 255 | $status = Status::newGood(); |
207 | 256 | |
208 | | - list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] ); |
| 257 | + $source = $this->resolveToFSPath( $params['src'] ); |
209 | 258 | if ( $source === null ) { |
210 | 259 | $status->fatal( 'backend-fail-invalidpath', $params['src'] ); |
211 | 260 | return $status; |
— | — | @@ -234,7 +283,7 @@ |
235 | 284 | protected function doCreateInternal( array $params ) { |
236 | 285 | $status = Status::newGood(); |
237 | 286 | |
238 | | - list( $c, $dest ) = $this->resolveStoragePathReal( $params['dst'] ); |
| 287 | + $dest = $this->resolveToFSPath( $params['dst'] ); |
239 | 288 | if ( $dest === null ) { |
240 | 289 | $status->fatal( 'backend-fail-invalidpath', $params['dst'] ); |
241 | 290 | return $status; |
— | — | @@ -276,8 +325,11 @@ |
277 | 326 | /** |
278 | 327 | * @see FileBackend::doPrepareInternal() |
279 | 328 | */ |
280 | | - protected function doPrepareInternal( $container, $dir, array $params ) { |
| 329 | + protected function doPrepareInternal( $fullCont, $dirRel, array $params ) { |
281 | 330 | $status = Status::newGood(); |
| 331 | + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); |
| 332 | + $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid |
| 333 | + $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; |
282 | 334 | if ( !wfMkdirParents( $dir ) ) { |
283 | 335 | $status->fatal( 'directorycreateerror', $params['dir'] ); |
284 | 336 | } elseif ( !is_writable( $dir ) ) { |
— | — | @@ -291,8 +343,11 @@ |
292 | 344 | /** |
293 | 345 | * @see FileBackend::doSecureInternal() |
294 | 346 | */ |
295 | | - protected function doSecureInternal( $container, $dir, array $params ) { |
| 347 | + protected function doSecureInternal( $fullCont, $dirRel, array $params ) { |
296 | 348 | $status = Status::newGood(); |
| 349 | + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); |
| 350 | + $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid |
| 351 | + $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; |
297 | 352 | if ( !wfMkdirParents( $dir ) ) { |
298 | 353 | $status->fatal( 'directorycreateerror', $params['dir'] ); |
299 | 354 | return $status; |
— | — | @@ -309,14 +364,13 @@ |
310 | 365 | } |
311 | 366 | // Add a .htaccess file to the root of the container... |
312 | 367 | if ( !empty( $params['noAccess'] ) ) { |
313 | | - list( $b, $container, $r ) = FileBackend::splitStoragePath( $params['dir'] ); |
314 | | - $dirRoot = $this->containerPaths[$container]; // real path |
| 368 | + $dirRoot = $this->resolveToFSPath( $params['dir'], '' ); |
315 | 369 | if ( !file_exists( "{$dirRoot}/.htaccess" ) ) { |
316 | 370 | wfSuppressWarnings(); |
317 | 371 | $ok = file_put_contents( "{$dirRoot}/.htaccess", "Deny from all\n" ); |
318 | 372 | wfRestoreWarnings(); |
319 | 373 | if ( !$ok ) { |
320 | | - $storeDir = "mwstore://{$this->name}/{$container}"; |
| 374 | + $storeDir = "mwstore://{$this->name}/{$shortCont}"; |
321 | 375 | $status->fatal( 'backend-fail-create', "$storeDir/.htaccess" ); |
322 | 376 | return $status; |
323 | 377 | } |
— | — | @@ -328,8 +382,11 @@ |
329 | 383 | /** |
330 | 384 | * @see FileBackend::doCleanInternal() |
331 | 385 | */ |
332 | | - protected function doCleanInternal( $container, $dir, array $params ) { |
| 386 | + protected function doCleanInternal( $fullCont, $dirRel, array $params ) { |
333 | 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; |
334 | 391 | wfSuppressWarnings(); |
335 | 392 | if ( is_dir( $dir ) ) { |
336 | 393 | rmdir( $dir ); // remove directory if empty |
— | — | @@ -342,17 +399,13 @@ |
343 | 400 | * @see FileBackend::doFileExists() |
344 | 401 | */ |
345 | 402 | protected function doGetFileStat( array $params ) { |
346 | | - list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] ); |
| 403 | + $source = $this->resolveToFSPath( $params['src'] ); |
347 | 404 | if ( $source === null ) { |
348 | 405 | return false; // invalid storage path |
349 | 406 | } |
350 | 407 | |
351 | 408 | wfSuppressWarnings(); |
352 | | - if ( is_file( $source ) ) { // regular file? |
353 | | - $stat = stat( $source ); |
354 | | - } else { |
355 | | - $stat = false; |
356 | | - } |
| 409 | + $stat = is_file( $source ) ? stat( $source ) : false; // regular files only |
357 | 410 | wfRestoreWarnings(); |
358 | 411 | |
359 | 412 | if ( $stat ) { |
— | — | @@ -368,7 +421,10 @@ |
369 | 422 | /** |
370 | 423 | * @see FileBackend::getFileListInternal() |
371 | 424 | */ |
372 | | - public function getFileListInternal( $container, $dir, array $params ) { |
| 425 | + public function getFileListInternal( $fullCont, $dirRel, array $params ) { |
| 426 | + list( $b, $shortCont, $r ) = FileBackend::splitStoragePath( $params['dir'] ); |
| 427 | + $contRoot = $this->containerFSRoot( $shortCont, $fullCont ); // must be valid |
| 428 | + $dir = ( $dirRel != '' ) ? "{$contRoot}/{$dirRel}" : $contRoot; |
373 | 429 | wfSuppressWarnings(); |
374 | 430 | $exists = is_dir( $dir ); |
375 | 431 | wfRestoreWarnings(); |
— | — | @@ -388,7 +444,7 @@ |
389 | 445 | * @see FileBackend::getLocalReference() |
390 | 446 | */ |
391 | 447 | public function getLocalReference( array $params ) { |
392 | | - list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] ); |
| 448 | + $source = $this->resolveToFSPath( $params['src'] ); |
393 | 449 | if ( $source === null ) { |
394 | 450 | return null; |
395 | 451 | } |
— | — | @@ -399,7 +455,7 @@ |
400 | 456 | * @see FileBackend::getLocalCopy() |
401 | 457 | */ |
402 | 458 | public function getLocalCopy( array $params ) { |
403 | | - list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] ); |
| 459 | + $source = $this->resolveToFSPath( $params['src'] ); |
404 | 460 | if ( $source === null ) { |
405 | 461 | return null; |
406 | 462 | } |
— | — | @@ -470,7 +526,8 @@ |
471 | 527 | public function current() { |
472 | 528 | // Return only the relative path and normalize slashes to FileBackend-style |
473 | 529 | // Make sure to use the realpath since the suffix is based upon that |
474 | | - return str_replace( '\\', '/', substr( realpath($this->iter->current()), $this->suffixStart ) ); |
| 530 | + return str_replace( '\\', '/', |
| 531 | + substr( realpath( $this->iter->current() ), $this->suffixStart ) ); |
475 | 532 | } |
476 | 533 | |
477 | 534 | public function key() { |