r108353 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r108352‎ | r108353 | r108354 >
Date:08:40, 8 January 2012
Author:aaron
Status:ok (Comments)
Tags:filebackend, todo 
Comment:
In FileBackend:
* Added getFileSize()/getFileStat() functions. Refactored some functions to use the stat function for better reuse and caching/consistency.
* Refactored streamFile() to allow for subclasses to avoid local file copying with less duplication. Also make last-modified check actually work since we always get the timestamp of the original file.
* Renamed 'ignoreErrors' parameter to 'force'.
In FileBackendMultiWrite:
* Simplified how read ops are done (use 'master' backend for consistency).
* Added consistency check to doOperationsInternal() to check if the files are synced.
* Various fixes after testing.
In StreamFile:
* Split out prepareForStream() function from stream() in StreamFile for code reuse.
In FileBackendTest:
* Properly cover FileBackendMultiWrite in tests.
* Various test improvements.
Modified paths:
  • /trunk/phase3/includes/StreamFile.php (modified) (history)
  • /trunk/phase3/includes/filerepo/FileRepo.php (modified) (history)
  • /trunk/phase3/includes/filerepo/backend/FSFileBackend.php (modified) (history)
  • /trunk/phase3/includes/filerepo/backend/FileBackend.php (modified) (history)
  • /trunk/phase3/includes/filerepo/backend/FileBackendMultiWrite.php (modified) (history)
  • /trunk/phase3/includes/filerepo/backend/FileOp.php (modified) (history)
  • /trunk/phase3/includes/filerepo/file/File.php (modified) (history)
  • /trunk/phase3/languages/messages/MessagesEn.php (modified) (history)
  • /trunk/phase3/maintenance/language/messages.inc (modified) (history)
  • /trunk/phase3/tests/phpunit/includes/filerepo/FileBackendTest.php (modified) (history)

Diff [purge]

Index: trunk/phase3/maintenance/language/messages.inc
@@ -1363,7 +1363,8 @@
13641364 'backend-fail-closetemp',
13651365 'backend-fail-read',
13661366 'backend-fail-create',
1367 - 'backend-fail-readonly'
 1367+ 'backend-fail-readonly',
 1368+ 'backend-fail-synced'
13681369 ),
13691370
13701371 'lockmanager-errors' => array(
Index: trunk/phase3/tests/phpunit/includes/filerepo/FileBackendTest.php
@@ -7,15 +7,16 @@
88
99 function setUp() {
1010 parent::setUp();
11 - $this->backend = new FSFileBackend( array(
 11+ $tmpDir = wfTempDir() . '/' . time() . '-' . mt_rand();
 12+ $this->singleBackend = new FSFileBackend( array(
1213 'name' => 'localtesting',
1314 'lockManager' => 'fsLockManager',
1415 'containerPaths' => array(
15 - 'cont1' => wfTempDir() . '/localtesting/cont1',
16 - 'cont2' => wfTempDir() . '/localtesting/cont2' )
 16+ 'cont1' => "$tmpDir/localtesting/cont1",
 17+ 'cont2' => "$tmpDir/localtesting/cont2" )
1718 ) );
1819 $this->multiBackend = new FileBackendMultiWrite( array(
19 - 'name' => 'localtestingmulti',
 20+ 'name' => 'localtesting',
2021 'lockManager' => 'fsLockManager',
2122 'backends' => array(
2223 array(
@@ -23,8 +24,8 @@
2425 'class' => 'FSFileBackend',
2526 'lockManager' => 'nullLockManager',
2627 'containerPaths' => array(
27 - 'cont1' => wfTempDir() . '/localtestingmulti1/cont1',
28 - 'cont2' => wfTempDir() . '/localtestingmulti1/cont2' ),
 28+ 'cont1' => "$tmpDir/localtestingmulti1/cont1",
 29+ 'cont2' => "$tmpDir/localtestingmulti1/cont2" ),
2930 'isMultiMaster' => false
3031 ),
3132 array(
@@ -32,8 +33,8 @@
3334 'class' => 'FSFileBackend',
3435 'lockManager' => 'nullLockManager',
3536 'containerPaths' => array(
36 - 'cont1' => wfTempDir() . '/localtestingmulti2/cont1',
37 - 'cont2' => wfTempDir() . '/localtestingmulti2/cont2' ),
 37+ 'cont1' => "$tmpDir/localtestingmulti2/cont1",
 38+ 'cont2' => "$tmpDir/localtestingmulti2/cont2" ),
3839 'isMultiMaster' => true
3940 )
4041 )
@@ -41,10 +42,14 @@
4243 $this->filesToPrune = $this->pathsToPrune = array();
4344 }
4445
45 - private function singleBasePath() {
 46+ private function baseStorePath() {
4647 return 'mwstore://localtesting';
4748 }
4849
 50+ private function backendClass() {
 51+ return get_class( $this->backend );
 52+ }
 53+
4954 /**
5055 * @dataProvider provider_testStore
5156 */
@@ -52,29 +57,45 @@
5358 $this->filesToPrune[] = $source;
5459 $this->pathsToPrune[] = $dest;
5560
 61+ $this->backend = $this->singleBackend;
 62+ $this->doTestStore( $op, $source, $dest );
 63+ $this->tearDownFiles();
 64+
 65+ $this->backend = $this->multiBackend;
 66+ $this->doTestStore( $op, $source, $dest );
 67+ $this->tearDownFiles();
 68+ }
 69+
 70+ function doTestStore( $op, $source, $dest ) {
 71+ $backendName = $this->backendClass();
 72+
5673 file_put_contents( $source, "Unit test file" );
5774 $status = $this->backend->doOperation( $op );
5875
 76+ $this->assertEquals( array(), $status->errors,
 77+ "Store from $source to $dest succeeded without warnings ($backendName)." );
5978 $this->assertEquals( true, $status->isOK(),
60 - "Store from $source to $dest succeeded." );
61 - $this->assertEquals( true, $status->isGood(),
62 - "Store from $source to $dest succeeded without warnings." );
 79+ "Store from $source to $dest succeeded ($backendName)." );
6380 $this->assertEquals( true, file_exists( $source ),
64 - "Source file $source still exists." );
 81+ "Source file $source still exists ($backendName)." );
6582 $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
66 - "Destination file $dest exists." );
 83+ "Destination file $dest exists ($backendName)." );
6784
 85+ $this->assertEquals( filesize( $source ),
 86+ $this->backend->getFileSize( array( 'src' => $dest ) ),
 87+ "Destination file $dest has correct size ($backendName)." );
 88+
6889 $props1 = FSFile::getPropsFromPath( $source );
6990 $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
7091 $this->assertEquals( $props1, $props2,
71 - "Source and destination have the same props." );
 92+ "Source and destination have the same props ($backendName)." );
7293 }
7394
7495 public function provider_testStore() {
7596 $cases = array();
7697
7798 $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath();
78 - $toPath = $this->singleBasePath() . '/cont1/fun/obj1.txt';
 99+ $toPath = $this->baseStorePath() . '/cont1/fun/obj1.txt';
79100 $op = array( 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath );
80101 $cases[] = array(
81102 $op, // operation
@@ -99,31 +120,49 @@
100121 $this->pathsToPrune[] = $source;
101122 $this->pathsToPrune[] = $dest;
102123
 124+ $this->backend = $this->singleBackend;
 125+ $this->doTestCopy( $op, $source, $dest );
 126+ $this->tearDownFiles();
 127+
 128+ $this->backend = $this->multiBackend;
 129+ $this->doTestCopy( $op, $source, $dest );
 130+ $this->tearDownFiles();
 131+ }
 132+
 133+ function doTestCopy( $op, $source, $dest ) {
 134+ $backendName = $this->backendClass();
 135+
103136 $status = $this->backend->doOperation(
104137 array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
105 - $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." );
 138+ $this->assertEquals( true, $status->isOK(),
 139+ "Creation of file at $source succeeded ($backendName)." );
106140
107141 $status = $this->backend->doOperation( $op );
 142+ $this->assertEquals( array(), $status->errors,
 143+ "Copy from $source to $dest succeeded without warnings ($backendName)." );
108144 $this->assertEquals( true, $status->isOK(),
109 - "Copy from $source to $dest succeeded." );
110 - $this->assertEquals( true, $status->isGood(),
111 - "Copy from $source to $dest succeeded without warnings." );
 145+ "Copy from $source to $dest succeeded ($backendName)." );
112146 $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $source ) ),
113 - "Source file $source still exists." );
 147+ "Source file $source still exists ($backendName)." );
114148 $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
115 - "Destination file $dest exists after copy." );
 149+ "Destination file $dest exists after copy ($backendName)." );
116150
 151+ $this->assertEquals(
 152+ $this->backend->getFileSize( array( 'src' => $source ) ),
 153+ $this->backend->getFileSize( array( 'src' => $dest ) ),
 154+ "Destination file $dest has correct size ($backendName)." );
 155+
117156 $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
118157 $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
119158 $this->assertEquals( $props1, $props2,
120 - "Source and destination have the same props." );
 159+ "Source and destination have the same props ($backendName)." );
121160 }
122161
123162 public function provider_testCopy() {
124163 $cases = array();
125164
126 - $source = $this->singleBasePath() . '/cont1/file.txt';
127 - $dest = $this->singleBasePath() . '/cont2/fileMoved.txt';
 165+ $source = $this->baseStorePath() . '/cont1/file.txt';
 166+ $dest = $this->baseStorePath() . '/cont2/fileMoved.txt';
128167
129168 $op = array( 'op' => 'copy', 'src' => $source, 'dst' => $dest );
130169 $cases[] = array(
@@ -149,33 +188,51 @@
150189 $this->pathsToPrune[] = $source;
151190 $this->pathsToPrune[] = $dest;
152191
 192+ $this->backend = $this->singleBackend;
 193+ $this->doTestMove( $op, $source, $dest );
 194+ $this->tearDownFiles();
 195+
 196+ $this->backend = $this->multiBackend;
 197+ $this->doTestMove( $op, $source, $dest );
 198+ $this->tearDownFiles();
 199+ }
 200+
 201+ public function doTestMove( $op, $source, $dest ) {
 202+ $backendName = $this->backendClass();
 203+
153204 $status = $this->backend->doOperation(
154205 array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
155 - $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." );
 206+ $this->assertEquals( true, $status->isOK(),
 207+ "Creation of file at $source succeeded ($backendName)." );
156208
157209 $status = $this->backend->doOperation( $op );
 210+ $this->assertEquals( array(), $status->errors,
 211+ "Move from $source to $dest succeeded without warnings ($backendName)." );
158212 $this->assertEquals( true, $status->isOK(),
159 - "Move from $source to $dest succeeded." );
160 - $this->assertEquals( true, $status->isGood(),
161 - "Move from $source to $dest succeeded without warnings." );
 213+ "Move from $source to $dest succeeded ($backendName)." );
162214 $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
163 - "Source file $source does not still exists." );
 215+ "Source file $source does not still exists ($backendName)." );
164216 $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
165 - "Destination file $dest exists after move." );
 217+ "Destination file $dest exists after move ($backendName)." );
166218
 219+ $this->assertNotEquals(
 220+ $this->backend->getFileSize( array( 'src' => $source ) ),
 221+ $this->backend->getFileSize( array( 'src' => $dest ) ),
 222+ "Destination file $dest has correct size ($backendName)." );
 223+
167224 $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
168225 $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
169226 $this->assertEquals( false, $props1['fileExists'],
170 - "Source file does not exist accourding to props." );
 227+ "Source file does not exist accourding to props ($backendName)." );
171228 $this->assertEquals( true, $props2['fileExists'],
172 - "Destination file exists accourding to props." );
 229+ "Destination file exists accourding to props ($backendName)." );
173230 }
174231
175232 public function provider_testMove() {
176233 $cases = array();
177234
178 - $source = $this->singleBasePath() . '/cont1/file.txt';
179 - $dest = $this->singleBasePath() . '/cont2/fileMoved.txt';
 235+ $source = $this->baseStorePath() . '/cont1/file.txt';
 236+ $dest = $this->baseStorePath() . '/cont2/fileMoved.txt';
180237
181238 $op = array( 'op' => 'move', 'src' => $source, 'dst' => $dest );
182239 $cases[] = array(
@@ -200,31 +257,52 @@
201258 public function testDelete( $op, $source, $withSource, $okStatus ) {
202259 $this->pathsToPrune[] = $source;
203260
 261+ $this->backend = $this->singleBackend;
 262+ $this->doTestDelete( $op, $source, $withSource, $okStatus );
 263+ $this->tearDownFiles();
 264+
 265+ $this->backend = $this->multiBackend;
 266+ $this->doTestDelete( $op, $source, $withSource, $okStatus );
 267+ $this->tearDownFiles();
 268+ }
 269+
 270+ public function doTestDelete( $op, $source, $withSource, $okStatus ) {
 271+ $backendName = $this->backendClass();
 272+
204273 if ( $withSource ) {
205274 $status = $this->backend->doOperation(
206275 array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
207 - $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." );
 276+ $this->assertEquals( true, $status->isOK(),
 277+ "Creation of file at $source succeeded ($backendName)." );
208278 }
209279
210280 $status = $this->backend->doOperation( $op );
211281 if ( $okStatus ) {
212 - $this->assertEquals( true, $status->isOK(), "Deletion of file at $source succeeded." );
 282+ $this->assertEquals( array(), $status->errors,
 283+ "Deletion of file at $source succeeded without warnings ($backendName)." );
 284+ $this->assertEquals( true, $status->isOK(),
 285+ "Deletion of file at $source succeeded ($backendName)." );
213286 } else {
214 - $this->assertEquals( false, $status->isOK(), "Deletion of file at $source failed." );
 287+ $this->assertEquals( false, $status->isOK(),
 288+ "Deletion of file at $source failed ($backendName)." );
215289 }
216290
217291 $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
218 - "Source file $source does not exist after move." );
 292+ "Source file $source does not exist after move ($backendName)." );
219293
 294+ $this->assertFalse(
 295+ $this->backend->getFileSize( array( 'src' => $source ) ),
 296+ "Source file $source has correct size (false) ($backendName)." );
 297+
220298 $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
221 - $this->assertEquals( false, $props1['fileExists'],
222 - "Source file $source does not exist according to props." );
 299+ $this->assertFalse( $props1['fileExists'],
 300+ "Source file $source does not exist according to props ($backendName)." );
223301 }
224302
225303 public function provider_testDelete() {
226304 $cases = array();
227305
228 - $source = $this->singleBasePath() . '/cont1/myfacefile.txt';
 306+ $source = $this->baseStorePath() . '/cont1/myfacefile.txt';
229307
230308 $op = array( 'op' => 'delete', 'src' => $source );
231309 $cases[] = array(
@@ -258,32 +336,55 @@
259337 public function testCreate( $op, $dest, $alreadyExists, $okStatus, $newSize ) {
260338 $this->pathsToPrune[] = $dest;
261339
 340+ $this->backend = $this->singleBackend;
 341+ $this->doTestCreate( $op, $dest, $alreadyExists, $okStatus, $newSize );
 342+ $this->tearDownFiles();
 343+
 344+ $this->backend = $this->multiBackend;
 345+ $this->doTestCreate( $op, $dest, $alreadyExists, $okStatus, $newSize );
 346+ $this->tearDownFiles();
 347+ }
 348+
 349+ public function doTestCreate( $op, $dest, $alreadyExists, $okStatus, $newSize ) {
 350+ $backendName = $this->backendClass();
 351+
262352 $oldText = 'blah...blah...waahwaah';
263353 if ( $alreadyExists ) {
264354 $status = $this->backend->doOperation(
265355 array( 'op' => 'create', 'content' => $oldText, 'dst' => $dest ) );
266 - $this->assertEquals( true, $status->isOK(), "Creation of file at $dest succeeded." );
 356+ $this->assertEquals( true, $status->isOK(),
 357+ "Creation of file at $dest succeeded ($backendName)." );
267358 }
268359
269360 $status = $this->backend->doOperation( $op );
270361 if ( $okStatus ) {
271 - $this->assertEquals( true, $status->isOK(), "Creation of file at $dest succeeded." );
 362+ $this->assertEquals( array(), $status->errors,
 363+ "Creation of file at $dest succeeded without warnings ($backendName)." );
 364+ $this->assertEquals( true, $status->isOK(),
 365+ "Creation of file at $dest succeeded ($backendName)." );
272366 } else {
273 - $this->assertEquals( false, $status->isOK(), "Creation of file at $dest failed." );
 367+ $this->assertEquals( false, $status->isOK(),
 368+ "Creation of file at $dest failed ($backendName)." );
274369 }
275370
276371 $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
277 - "Dest file $dest exists after creation." );
 372+ "Destination file $dest exists after creation ($backendName)." );
278373
279374 $props1 = $this->backend->getFileProps( array( 'src' => $dest ) );
280375 $this->assertEquals( true, $props1['fileExists'],
281 - "Dest file $dest exists according to props." );
 376+ "Destination file $dest exists according to props ($backendName)." );
282377 if ( $okStatus ) { // file content is what we saved
283378 $this->assertEquals( $newSize, $props1['size'],
284 - "Dest file $dest has expected size according to props." );
 379+ "Destination file $dest has expected size according to props ($backendName)." );
 380+ $this->assertEquals( $newSize,
 381+ $this->backend->getFileSize( array( 'src' => $dest ) ),
 382+ "Destination file $dest has correct size ($backendName)." );
285383 } else { // file content is some other previous text
286384 $this->assertEquals( strlen( $oldText ), $props1['size'],
287 - "Dest file $dest has different size that given text according to props." );
 385+ "Destination file $dest has original size according to props ($backendName)." );
 386+ $this->assertEquals( strlen( $oldText ),
 387+ $this->backend->getFileSize( array( 'src' => $dest ) ),
 388+ "Destination file $dest has original size according to props ($backendName)." );
288389 }
289390 }
290391
@@ -293,7 +394,7 @@
294395 public function provider_testCreate() {
295396 $cases = array();
296397
297 - $source = $this->singleBasePath() . '/cont2/myspacefile.txt';
 398+ $source = $this->baseStorePath() . '/cont2/myspacefile.txt';
298399
299400 $dummyText = 'hey hey';
300401 $op = array( 'op' => 'create', 'content' => $dummyText, 'dst' => $source );
@@ -330,8 +431,21 @@
331432 */
332433 public function testConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
333434 $this->pathsToPrune = array_merge( $this->pathsToPrune, $srcs );
334 - $this->pathsToPrune[] = $op['dst'];
 435+ $this->filesToPrune[] = $op['dst'];
335436
 437+ $this->backend = $this->singleBackend;
 438+ $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
 439+ $this->tearDownFiles();
 440+
 441+ # FIXME
 442+ #$this->backend = $this->multiBackend;
 443+ #$this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
 444+ #$this->tearDownFiles();
 445+ }
 446+
 447+ public function doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
 448+ $backendName = $this->backendClass();
 449+
336450 $expContent = '';
337451 // Create sources
338452 $ops = array();
@@ -345,40 +459,49 @@
346460 }
347461 $status = $this->backend->doOperations( $ops );
348462
349 - $this->assertEquals( true, $status->isOK(), "Creation of source files succeeded." );
 463+ $this->assertEquals( true, $status->isOK(),
 464+ "Creation of source files succeeded ($backendName)." );
350465
351466 $dest = $op['dst'];
352467 if ( $alreadyExists ) {
353468 $ok = file_put_contents( $dest, 'blah...blah...waahwaah' ) !== false;
354 - $this->assertEquals( true, $ok, "Creation of file at $dest succeeded." );
 469+ $this->assertEquals( true, $ok,
 470+ "Creation of file at $dest succeeded ($backendName)." );
355471 } else {
356472 $ok = file_put_contents( $dest, '' ) !== false;
357 - $this->assertEquals( true, $ok, "Creation of 0-byte file at $dest succeeded." );
 473+ $this->assertEquals( true, $ok,
 474+ "Creation of 0-byte file at $dest succeeded ($backendName)." );
358475 }
359476
360477 // Combine them
361478 $status = $this->backend->doOperation( $op );
362479 if ( $okStatus ) {
363 - $this->assertEquals( true, $status->isOK(), "Creation of concat file at $dest succeeded." );
 480+ $this->assertEquals( array(), $status->errors,
 481+ "Creation of concat file at $dest succeeded without warnings ($backendName)." );
 482+ $this->assertEquals( true, $status->isOK(),
 483+ "Creation of concat file at $dest succeeded ($backendName)." );
364484 } else {
365 - $this->assertEquals( false, $status->isOK(), "Creation of concat file at $dest failed." );
 485+ $this->assertEquals( false, $status->isOK(),
 486+ "Creation of concat file at $dest failed ($backendName)." );
366487 }
367488
368489 if ( $okStatus ) {
369490 $this->assertEquals( true, is_file( $dest ),
370 - "Dest concat file $dest exists after creation." );
 491+ "Dest concat file $dest exists after creation ($backendName)." );
371492 } else {
372493 $this->assertEquals( true, is_file( $dest ),
373 - "Dest concat file $dest exists after failed creation." );
 494+ "Dest concat file $dest exists after failed creation ($backendName)." );
374495 }
375496
376497 $contents = file_get_contents( $dest );
377 - $this->assertNotEquals( false, $contents, "File at $dest exists." );
 498+ $this->assertNotEquals( false, $contents, "File at $dest exists ($backendName)." );
378499
379500 if ( $okStatus ) {
380 - $this->assertEquals( $expContent, $contents, "Concat file at $dest has correct contents." );
 501+ $this->assertEquals( $expContent, $contents,
 502+ "Concat file at $dest has correct contents ($backendName)." );
381503 } else {
382 - $this->assertNotEquals( $expContent, $contents, "Concat file at $dest has correct contents." );
 504+ $this->assertNotEquals( $expContent, $contents,
 505+ "Concat file at $dest has correct contents ($backendName)." );
383506 }
384507 }
385508
@@ -388,16 +511,16 @@
389512 $rand = mt_rand( 0, 2000000000 ) . time();
390513 $dest = wfTempDir() . "/randomfile!$rand.txt";
391514 $srcs = array(
392 - $this->singleBasePath() . '/cont1/file1.txt',
393 - $this->singleBasePath() . '/cont1/file2.txt',
394 - $this->singleBasePath() . '/cont1/file3.txt',
395 - $this->singleBasePath() . '/cont1/file4.txt',
396 - $this->singleBasePath() . '/cont1/file5.txt',
397 - $this->singleBasePath() . '/cont1/file6.txt',
398 - $this->singleBasePath() . '/cont1/file7.txt',
399 - $this->singleBasePath() . '/cont1/file8.txt',
400 - $this->singleBasePath() . '/cont1/file9.txt',
401 - $this->singleBasePath() . '/cont1/file10.txt'
 515+ $this->baseStorePath() . '/cont1/file1.txt',
 516+ $this->baseStorePath() . '/cont1/file2.txt',
 517+ $this->baseStorePath() . '/cont1/file3.txt',
 518+ $this->baseStorePath() . '/cont1/file4.txt',
 519+ $this->baseStorePath() . '/cont1/file5.txt',
 520+ $this->baseStorePath() . '/cont1/file6.txt',
 521+ $this->baseStorePath() . '/cont1/file7.txt',
 522+ $this->baseStorePath() . '/cont1/file8.txt',
 523+ $this->baseStorePath() . '/cont1/file9.txt',
 524+ $this->baseStorePath() . '/cont1/file10.txt'
402525 );
403526 $content = array(
404527 'egfage',
@@ -425,7 +548,7 @@
426549 $op, // operation
427550 $srcs, // sources
428551 $content, // content for each source
429 - true, // no dest already exists
 552+ true, // dest already exists
430553 false, // succeeds
431554 );
432555
@@ -438,20 +561,38 @@
439562 public function testGetFileContents( $src, $content ) {
440563 $this->pathsToPrune[] = $src;
441564
 565+ $this->backend = $this->singleBackend;
 566+ $this->doTestGetFileContents( $src, $content );
 567+ $this->tearDownFiles();
 568+
 569+ $this->backend = $this->multiBackend;
 570+ $this->doTestGetFileContents( $src, $content );
 571+ $this->tearDownFiles();
 572+ }
 573+
 574+ /**
 575+ * @dataProvider provider_testGetFileContents
 576+ */
 577+ public function doTestGetFileContents( $src, $content ) {
 578+ $backendName = $this->backendClass();
 579+
442580 $status = $this->backend->doOperation(
443581 array( 'op' => 'create', 'content' => $content, 'dst' => $src ) );
444 - $this->assertEquals( true, $status->isOK(), "Creation of file at $src succeeded." );
 582+ $this->assertEquals( true, $status->isOK(),
 583+ "Creation of file at $src succeeded ($backendName)." );
445584
446585 $newContents = $this->backend->getFileContents( array( 'src' => $src ) );
447 - $this->assertNotEquals( false, $newContents, "Read of file at $src succeeded." );
 586+ $this->assertNotEquals( false, $newContents,
 587+ "Read of file at $src succeeded ($backendName)." );
448588
449 - $this->assertEquals( $content, $newContents, "Contents read match data at $src." );
 589+ $this->assertEquals( $content, $newContents,
 590+ "Contents read match data at $src ($backendName)." );
450591 }
451592
452593 function provider_testGetFileContents() {
453594 $cases = array();
454595
455 - $base = $this->singleBasePath();
 596+ $base = $this->baseStorePath();
456597 $cases[] = array( "$base/cont1/b/z/some_file.txt", "some file contents" );
457598 $cases[] = array( "$base/cont1/b/some-other_file.txt", "more file contents" );
458599
@@ -464,21 +605,35 @@
465606 public function testGetLocalCopy( $src, $content ) {
466607 $this->pathsToPrune[] = $src;
467608
 609+ $this->backend = $this->singleBackend;
 610+ $this->doTestGetLocalCopy( $src, $content );
 611+ $this->tearDownFiles();
 612+
 613+ $this->backend = $this->multiBackend;
 614+ $this->doTestGetLocalCopy( $src, $content );
 615+ $this->tearDownFiles();
 616+ }
 617+
 618+ public function doTestGetLocalCopy( $src, $content ) {
 619+ $backendName = $this->backendClass();
 620+
468621 $status = $this->backend->doOperation(
469622 array( 'op' => 'create', 'content' => $content, 'dst' => $src ) );
470 - $this->assertEquals( true, $status->isOK(), "Creation of file at $src succeeded." );
 623+ $this->assertEquals( true, $status->isOK(),
 624+ "Creation of file at $src succeeded ($backendName)." );
471625
472626 $tmpFile = $this->backend->getLocalCopy( array( 'src' => $src ) );
473 - $this->assertNotNull( $tmpFile, "Creation of local copy of $src succeeded." );
 627+ $this->assertNotNull( $tmpFile,
 628+ "Creation of local copy of $src succeeded ($backendName)." );
474629
475630 $contents = file_get_contents( $tmpFile->getPath() );
476 - $this->assertNotEquals( false, $contents, "Local copy of $src exists." );
 631+ $this->assertNotEquals( false, $contents, "Local copy of $src exists ($backendName)." );
477632 }
478633
479634 function provider_testGetLocalCopy() {
480635 $cases = array();
481636
482 - $base = $this->singleBasePath();
 637+ $base = $this->baseStorePath();
483638 $cases[] = array( "$base/cont1/a/z/some_file.txt", "some file contents" );
484639 $cases[] = array( "$base/cont1/a/some-other_file.txt", "more file contents" );
485640
@@ -491,21 +646,35 @@
492647 public function testGetLocalReference( $src, $content ) {
493648 $this->pathsToPrune[] = $src;
494649
 650+ $this->backend = $this->singleBackend;
 651+ $this->doTestGetLocalReference( $src, $content );
 652+ $this->tearDownFiles();
 653+
 654+ $this->backend = $this->multiBackend;
 655+ $this->doTestGetLocalReference( $src, $content );
 656+ $this->tearDownFiles();
 657+ }
 658+
 659+ public function doTestGetLocalReference( $src, $content ) {
 660+ $backendName = $this->backendClass();
 661+
495662 $status = $this->backend->doOperation(
496663 array( 'op' => 'create', 'content' => $content, 'dst' => $src ) );
497 - $this->assertEquals( true, $status->isOK(), "Creation of file at $src succeeded." );
 664+ $this->assertEquals( true, $status->isOK(),
 665+ "Creation of file at $src succeeded ($backendName)." );
498666
499667 $tmpFile = $this->backend->getLocalReference( array( 'src' => $src ) );
500 - $this->assertNotNull( $tmpFile, "Creation of local copy of $src succeeded." );
 668+ $this->assertNotNull( $tmpFile,
 669+ "Creation of local copy of $src succeeded ($backendName)." );
501670
502671 $contents = file_get_contents( $tmpFile->getPath() );
503 - $this->assertNotEquals( false, $contents, "Local copy of $src exists." );
 672+ $this->assertNotEquals( false, $contents, "Local copy of $src exists ($backendName)." );
504673 }
505674
506675 function provider_testGetLocalReference() {
507676 $cases = array();
508677
509 - $base = $this->singleBasePath();
 678+ $base = $this->baseStorePath();
510679 $cases[] = array( "$base/cont1/a/z/some_file.txt", "some file contents" );
511680 $cases[] = array( "$base/cont1/a/some-other_file.txt", "more file contents" );
512681
@@ -521,7 +690,19 @@
522691 // @TODO: testDoOperations
523692
524693 public function testGetFileList() {
525 - $base = $this->singleBasePath();
 694+ $this->backend = $this->singleBackend;
 695+ $this->doTestGetFileList();
 696+ $this->tearDownFiles();
 697+
 698+ $this->backend = $this->multiBackend;
 699+ $this->doTestGetFileList();
 700+ $this->tearDownFiles();
 701+ }
 702+
 703+ public function doTestGetFileList() {
 704+ $backendName = $this->backendClass();
 705+
 706+ $base = $this->baseStorePath();
526707 $files = array(
527708 "$base/cont1/test1.txt",
528709 "$base/cont1/test2.txt",
@@ -546,7 +727,8 @@
547728 $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
548729 }
549730 $status = $this->backend->doOperations( $ops );
550 - $this->assertEquals( true, $status->isOK(), "Creation of files succeeded." );
 731+ $this->assertEquals( true, $status->isOK(),
 732+ "Creation of files succeeded ($backendName)." );
551733
552734 // Expected listing
553735 $expected = array(
@@ -575,7 +757,7 @@
576758 }
577759 sort( $list );
578760
579 - $this->assertEquals( $expected, $list, "Correct file listing." );
 761+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
580762
581763 // Actual listing (with trailing slash)
582764 $list = array();
@@ -585,7 +767,7 @@
586768 }
587769 sort( $list );
588770
589 - $this->assertEquals( $expected, $list, "Correct file listing." );
 771+ $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
590772
591773 foreach ( $files as $file ) {
592774 $this->backend->doOperation( array( 'op' => 'delete', 'src' => "$base/$file" ) );
@@ -595,16 +777,16 @@
596778 foreach ( $iter as $iter ) {} // no errors
597779 }
598780
599 - function tearDown() {
600 - parent::tearDown();
 781+ function tearDownFiles() {
601782 foreach ( $this->filesToPrune as $file ) {
602783 @unlink( $file );
603784 }
604785 foreach ( $this->pathsToPrune as $file ) {
605786 $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
606 - $this->multiBackend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
607787 }
608 - $this->backend = $this->multiBackend = null;
609 - $this->filesToPrune = $this->pathsToPrune = array();
610788 }
 789+
 790+ function tearDown() {
 791+ parent::tearDown();
 792+ }
611793 }
Index: trunk/phase3/includes/filerepo/file/File.php
@@ -811,7 +811,7 @@
812812 // overriding File::getThumbPath() to use a different zone (e.g. 'temp').
813813 $status = $this->repo->getBackend()->store(
814814 array( 'src' => $tmpThumbPath, 'dst' => $thumbPath ),
815 - array( 'ignoreErrors' => 1, 'nonLocking' => 1, 'allowStale' => 1 )
 815+ array( 'force' => 1, 'nonLocking' => 1, 'allowStale' => 1 )
816816 );
817817 if ( $status->isOK() ) {
818818 $thumb->setStoragePath( $thumbPath );
Index: trunk/phase3/includes/filerepo/backend/FileBackendMultiWrite.php
@@ -12,22 +12,16 @@
1313 * At least one of the backends must be declared the "master" backend.
1414 *
1515 * Only use this class when transitioning from one storage system to another.
16 - *
17 - * The order that the backends are defined sets the priority of which
18 - * backend is read from or written to first. Functions like fileExists()
19 - * and getFileProps() will return information based on the first backend
20 - * that has the file. Special cases are listed below:
21 - * a) getFileTimestamp() will always check only the master backend to
22 - * avoid confusing and inconsistent results.
23 - *
24 - * All write operations are performed on all backends.
 16+ *
 17+ * Read operations are only done on the 'master' backend for consistency.
 18+ * All write operations are performed on all backends, in the order defined.
2519 * If an operation fails on one backend it will be rolled back from the others.
2620 *
2721 * @ingroup FileBackend
2822 */
2923 class FileBackendMultiWrite extends FileBackendBase {
3024 /** @var Array Prioritized list of FileBackend objects */
31 - protected $fileBackends = array(); // array of (backend index => backends)
 25+ protected $backends = array(); // array of (backend index => backends)
3226 protected $masterIndex = -1; // index of master backend
3327
3428 /**
@@ -49,7 +43,7 @@
5044 throw new MWException( 'No class given for a backend config.' );
5145 }
5246 $class = $config['class'];
53 - $this->fileBackends[$index] = new $class( $config );
 47+ $this->backends[$index] = new $class( $config );
5448 if ( !empty( $config['isMultiMaster'] ) ) {
5549 if ( $this->masterIndex >= 0 ) {
5650 throw new MWException( 'More than one master backend defined.' );
@@ -69,41 +63,52 @@
7064 $status = Status::newGood();
7165
7266 $performOps = array(); // list of FileOp objects
73 - $filesLockEx = $filesLockSh = array(); // storage paths to lock
 67+ $filesRead = $filesChanged = array(); // storage paths used
7468 // Build up a list of FileOps. The list will have all the ops
7569 // for one backend, then all the ops for the next, and so on.
7670 // These batches of ops are all part of a continuous array.
77 - // Also build up a list of files to lock...
78 - foreach ( $this->fileBackends as $index => $backend ) {
79 - $backendOps = $this->substOpPaths( $ops, $backend );
 71+ // Also build up a list of files read/changed...
 72+ foreach ( $this->backends as $index => $backend ) {
 73+ $backendOps = $this->substOpBatchPaths( $ops, $backend );
 74+ // Add on the operation batch for this backend
8075 $performOps = array_merge( $performOps, $backend->getOperations( $backendOps ) );
81 - if ( $index == 0 && empty( $opts['nonLocking'] ) ) {
82 - // Set "files to lock" from the first batch so we don't try to set all
83 - // locks two or three times over (depending on the number of backends).
84 - // A lock on one storage path is a lock on all the backends.
 76+ if ( $index == 0 ) { // first batch
 77+ // Get the files used for these operations. Each backend has a batch of
 78+ // the same operations, so we only need to get them from the first batch.
8579 foreach ( $performOps as $fileOp ) {
86 - $filesLockSh = array_merge( $filesLockSh, $fileOp->storagePathsRead() );
87 - $filesLockEx = array_merge( $filesLockEx, $fileOp->storagePathsChanged() );
 80+ $filesRead = array_merge( $filesRead, $fileOp->storagePathsRead() );
 81+ $filesChanged = array_merge( $filesChanged, $fileOp->storagePathsChanged() );
8882 }
89 - // Optimization: if doing an EX lock anyway, don't also set an SH one
90 - $filesLockSh = array_diff( $filesLockSh, $filesLockEx );
91 - // Lock the paths under the proxy backend's name
92 - $this->unsubstPaths( $filesLockSh );
93 - $this->unsubstPaths( $filesLockEx );
 83+ // Get the paths under the proxy backend's name
 84+ $this->unsubstPaths( $filesRead );
 85+ $this->unsubstPaths( $filesChanged );
9486 }
9587 }
9688
9789 // Try to lock those files for the scope of this function...
98 - $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
99 - $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
100 - if ( !$status->isOK() ) {
101 - return $status; // abort
 90+ if ( empty( $opts['nonLocking'] ) ) {
 91+ $filesLockSh = array_diff( $filesRead, $filesChanged ); // optimization
 92+ $filesLockEx = $filesChanged;
 93+ $scopeLockS = $this->getScopedFileLocks( $filesLockSh, LockManager::LOCK_UW, $status );
 94+ $scopeLockE = $this->getScopedFileLocks( $filesLockEx, LockManager::LOCK_EX, $status );
 95+ if ( !$status->isOK() ) {
 96+ return $status; // abort
 97+ }
10298 }
10399
104100 // Clear any cache entries (after locks acquired)
105 - foreach ( $this->fileBackends as $backend ) {
 101+ foreach ( $this->backends as $backend ) {
106102 $backend->clearCache();
107103 }
 104+
 105+ // Do a consistency check to see if the backends agree
 106+ if ( count( $this->backends ) > 1 ) {
 107+ $status->merge( $this->consistencyCheck( array_merge( $filesRead, $filesChanged ) ) );
 108+ if ( !$status->isOK() ) {
 109+ return $status; // abort
 110+ }
 111+ }
 112+
108113 // Actually attempt the operation batch...
109114 $status->merge( FileOp::attemptBatch( $performOps, $opts ) );
110115
@@ -111,18 +116,61 @@
112117 }
113118
114119 /**
 120+ * Check that a set of files are consistent across all internal backends
 121+ *
 122+ * @param $paths Array
 123+ * @return Status
 124+ */
 125+ public function consistencyCheck( array $paths ) {
 126+ $status = Status::newGood();
 127+
 128+ $mBackend = $this->backends[$this->masterIndex];
 129+ foreach ( array_unique( $paths ) as $path ) {
 130+ $params = array( 'src' => $path );
 131+ // Stat the file on the 'master' backend
 132+ $mStat = $mBackend->getFileStat( $this->substOpPaths( $params, $mBackend ) );
 133+ // Check of all clone backends agree with the master...
 134+ foreach ( $this->backends as $index => $cBackend ) {
 135+ if ( $index === $this->masterIndex ) {
 136+ continue; // master
 137+ }
 138+ $cStat = $cBackend->getFileStat( $this->substOpPaths( $params, $cBackend ) );
 139+ if ( $mStat ) { // file is in master
 140+ if ( !$cStat ) { // file should exist
 141+ $status->fatal( 'backend-fail-synced', $path );
 142+ } elseif ( $cStat['size'] != $mStat['size'] ) { // wrong size
 143+ $status->fatal( 'backend-fail-synced', $path );
 144+ } else {
 145+ $mTs = wfTimestamp( TS_UNIX, $mStat['mtime'] );
 146+ $cTs = wfTimestamp( TS_UNIX, $cStat['mtime'] );
 147+ if ( abs( $mTs - $cTs ) > 30 ) { // outdated file somewhere
 148+ $status->fatal( 'backend-fail-synced', $path );
 149+ }
 150+ }
 151+ } else { // file is not in master
 152+ if ( $cStat ) { // file should not exist
 153+ $status->fatal( 'backend-fail-synced', $path );
 154+ }
 155+ }
 156+ }
 157+ }
 158+
 159+ return $status;
 160+ }
 161+
 162+ /**
115163 * Substitute the backend name in storage path parameters
116 - * for a set of operations with a that of a given backend.
 164+ * for a set of operations with that of a given backend.
117165 *
118166 * @param $ops Array List of file operation arrays
119167 * @param $backend FileBackend
120168 * @return Array
121169 */
122 - protected function substOpPaths( array $ops, FileBackend $backend ) {
 170+ protected function substOpBatchPaths( array $ops, FileBackend $backend ) {
123171 $newOps = array(); // operations
124172 foreach ( $ops as $op ) {
125173 $newOp = $op; // operation
126 - foreach ( array( 'src', 'srcs', 'dst' ) as $par ) {
 174+ foreach ( array( 'src', 'srcs', 'dst', 'dir' ) as $par ) {
127175 if ( isset( $newOp[$par] ) ) {
128176 $newOp[$par] = preg_replace(
129177 '!^mwstore://' . preg_quote( $this->name ) . '/!',
@@ -137,6 +185,18 @@
138186 }
139187
140188 /**
 189+ * Same as substOpBatchPaths() but for a single operation
 190+ *
 191+ * @param $op File operation array
 192+ * @param $backend FileBackend
 193+ * @return Array
 194+ */
 195+ protected function substOpPaths( array $ops, FileBackend $backend ) {
 196+ $newOps = $this->substOpBatchPaths( array( $ops ), $backend );
 197+ return $newOps[0];
 198+ }
 199+
 200+ /**
141201 * Replace the backend part of storage paths with this backend's name
142202 *
143203 * @param &$paths Array
@@ -151,7 +211,7 @@
152212 /**
153213 * @see FileBackendBase::prepare()
154214 */
155 - function prepare( array $params ) {
 215+ public function prepare( array $params ) {
156216 $status = Status::newGood();
157217 foreach ( $this->backends as $backend ) {
158218 $realParams = $this->substOpPaths( $params, $backend );
@@ -163,7 +223,7 @@
164224 /**
165225 * @see FileBackendBase::secure()
166226 */
167 - function secure( array $params ) {
 227+ public function secure( array $params ) {
168228 $status = Status::newGood();
169229 foreach ( $this->backends as $backend ) {
170230 $realParams = $this->substOpPaths( $params, $backend );
@@ -175,7 +235,7 @@
176236 /**
177237 * @see FileBackendBase::clean()
178238 */
179 - function clean( array $params ) {
 239+ public function clean( array $params ) {
180240 $status = Status::newGood();
181241 foreach ( $this->backends as $backend ) {
182242 $realParams = $this->substOpPaths( $params, $backend );
@@ -187,134 +247,88 @@
188248 /**
189249 * @see FileBackendBase::fileExists()
190250 */
191 - function fileExists( array $params ) {
192 - # Hit all backends in case of failed operations (out of sync)
193 - foreach ( $this->backends as $backend ) {
194 - $realParams = $this->substOpPaths( $params, $backend );
195 - if ( $backend->fileExists( $realParams ) ) {
196 - return true;
197 - }
198 - }
199 - return false;
 251+ public function fileExists( array $params ) {
 252+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
 253+ return $this->backends[$this->masterIndex]->fileExists( $realParams );
200254 }
201255
202256 /**
203257 * @see FileBackendBase::getFileTimestamp()
204258 */
205 - function getFileTimestamp( array $params ) {
206 - // Skip non-master for consistent timestamps
 259+ public function getFileTimestamp( array $params ) {
207260 $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
208261 return $this->backends[$this->masterIndex]->getFileTimestamp( $realParams );
209262 }
210263
211264 /**
 265+ * @see FileBackendBase::getFileSize()
 266+ */
 267+ public function getFileSize( array $params ) {
 268+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
 269+ return $this->backends[$this->masterIndex]->getFileSize( $realParams );
 270+ }
 271+
 272+ /**
 273+ * @see FileBackendBase::getFileStat()
 274+ */
 275+ public function getFileStat( array $params ) {
 276+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
 277+ return $this->backends[$this->masterIndex]->getFileStat( $realParams );
 278+ }
 279+
 280+ /**
212281 * @see FileBackendBase::getFileContents()
213282 */
214 - function getFileContents( array $params ) {
215 - # Hit all backends in case of failed operations (out of sync)
216 - foreach ( $this->backends as $backend ) {
217 - $realParams = $this->substOpPaths( $params, $backend );
218 - $data = $backend->getFileContents( $realParams );
219 - if ( $data !== false ) {
220 - return $data;
221 - }
222 - }
223 - return false;
 283+ public function getFileContents( array $params ) {
 284+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
 285+ return $this->backends[$this->masterIndex]->getFileContents( $realParams );
224286 }
225287
226288 /**
227289 * @see FileBackendBase::getFileSha1Base36()
228290 */
229 - function getFileSha1Base36( array $params ) {
230 - # Hit all backends in case of failed operations (out of sync)
231 - foreach ( $this->backends as $backend ) {
232 - $realParams = $this->substOpPaths( $params, $backend );
233 - $hash = $backend->getFileSha1Base36( $realParams );
234 - if ( $hash !== false ) {
235 - return $hash;
236 - }
237 - }
238 - return false;
 291+ public function getFileSha1Base36( array $params ) {
 292+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
 293+ return $this->backends[$this->masterIndex]->getFileSha1Base36( $realParams );
239294 }
240295
241296 /**
242297 * @see FileBackendBase::getFileProps()
243298 */
244 - function getFileProps( array $params ) {
245 - # Hit all backends in case of failed operations (out of sync)
246 - foreach ( $this->backends as $backend ) {
247 - $realParams = $this->substOpPaths( $params, $backend );
248 - $props = $backend->getFileProps( $realParams );
249 - if ( $props !== null ) {
250 - return $props;
251 - }
252 - }
253 - return null;
 299+ public function getFileProps( array $params ) {
 300+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
 301+ return $this->backends[$this->masterIndex]->getFileProps( $realParams );
254302 }
255303
256304 /**
257305 * @see FileBackendBase::streamFile()
258306 */
259 - function streamFile( array $params ) {
260 - $status = Status::newGood();
261 - foreach ( $this->backends as $backend ) {
262 - $realParams = $this->substOpPaths( $params, $backend );
263 - $subStatus = $backend->streamFile( $realParams );
264 - $status->merge( $subStatus );
265 - if ( $subStatus->isOK() ) {
266 - // Pass isOK() despite fatals from other backends
267 - $status->setResult( true );
268 - return $status;
269 - } else { // failure
270 - if ( headers_sent() ) {
271 - return $status; // died mid-stream...so this is already fubar
272 - } elseif ( strval( ob_get_contents() ) !== '' ) {
273 - ob_clean(); // output was buffered but not sent; clear it
274 - }
275 - }
276 - }
277 - return $status;
 307+ public function streamFile( array $params ) {
 308+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
 309+ return $this->backends[$this->masterIndex]->streamFile( $realParams );
278310 }
279311
280312 /**
281313 * @see FileBackendBase::getLocalReference()
282314 */
283 - function getLocalReference( array $params ) {
284 - # Hit all backends in case of failed operations (out of sync)
285 - foreach ( $this->backends as $backend ) {
286 - $realParams = $this->substOpPaths( $params, $backend );
287 - $fsFile = $backend->getLocalReference( $realParams );
288 - if ( $fsFile ) {
289 - return $fsFile;
290 - }
291 - }
292 - return null;
 315+ public function getLocalReference( array $params ) {
 316+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
 317+ return $this->backends[$this->masterIndex]->getLocalReference( $realParams );
293318 }
294319
295320 /**
296321 * @see FileBackendBase::getLocalCopy()
297322 */
298 - function getLocalCopy( array $params ) {
299 - # Hit all backends in case of failed operations (out of sync)
300 - foreach ( $this->backends as $backend ) {
301 - $realParams = $this->substOpPaths( $params, $backend );
302 - $tmpFile = $backend->getLocalCopy( $realParams );
303 - if ( $tmpFile ) {
304 - return $tmpFile;
305 - }
306 - }
307 - return null;
 323+ public function getLocalCopy( array $params ) {
 324+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
 325+ return $this->backends[$this->masterIndex]->getLocalCopy( $realParams );
308326 }
309327
310328 /**
311329 * @see FileBackendBase::getFileList()
312330 */
313 - function getFileList( array $params ) {
314 - foreach ( $this->backends as $backend ) {
315 - # Get results from the first backend
316 - $realParams = $this->substOpPaths( $params, $backend );
317 - return $backend->getFileList( $realParams );
318 - }
319 - return array(); // sanity
 331+ public function getFileList( array $params ) {
 332+ $realParams = $this->substOpPaths( $params, $this->backends[$this->masterIndex] );
 333+ return $this->backends[$this->masterIndex]->getFileList( $realParams );
320334 }
321335 }
Index: trunk/phase3/includes/filerepo/backend/FileOp.php
@@ -84,7 +84,7 @@
8585 $status = Status::newGood();
8686
8787 $allowStale = isset( $opts['allowStale'] ) && $opts['allowStale'];
88 - $ignoreErrors = isset( $opts['ignoreErrors'] ) && $opts['ignoreErrors'];
 88+ $ignoreErrors = isset( $opts['force'] ) && $opts['force'];
8989 $predicates = FileOp::newPredicates(); // account for previous op in prechecks
9090 // Do pre-checks for each operation; abort on failure...
9191 foreach ( $performOps as $index => $fileOp ) {
Index: trunk/phase3/includes/filerepo/backend/FSFileBackend.php
@@ -339,27 +339,28 @@
340340 /**
341341 * @see FileBackend::doFileExists()
342342 */
343 - protected function doFileExists( array $params ) {
 343+ protected function doGetFileStat( array $params ) {
344344 list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] );
345345 if ( $source === null ) {
346346 return false; // invalid storage path
347347 }
 348+
348349 wfSuppressWarnings();
349 - $exists = is_file( $source );
 350+ if ( is_file( $source ) ) { // regular file?
 351+ $stat = stat( $source );
 352+ } else {
 353+ $stat = false;
 354+ }
350355 wfRestoreWarnings();
351 - return $exists;
352 - }
353356
354 - /**
355 - * @see FileBackend::doGetFileTimestamp()
356 - */
357 - public function doGetFileTimestamp( array $params ) {
358 - list( $c, $source ) = $this->resolveStoragePathReal( $params['src'] );
359 - if ( $source === null ) {
360 - return false; // invalid storage path
 357+ if ( $stat ) {
 358+ return array(
 359+ 'mtime' => wfTimestamp( TS_MW, $stat['mtime'] ),
 360+ 'size' => $stat['size']
 361+ );
 362+ } else {
 363+ return false;
361364 }
362 - $fsFile = new FSFile( $source );
363 - return $fsFile->getTimestamp();
364365 }
365366
366367 /**
Index: trunk/phase3/includes/filerepo/backend/FileBackend.php
@@ -152,20 +152,20 @@
153153 * contents as the new contents to be written there.
154154 *
155155 * $opts is an associative of boolean flags, including:
156 - * 'ignoreErrors' : Errors that would normally cause a rollback do not.
 156+ * 'force' : Errors that would normally cause a rollback do not.
157157 * The remaining operations are still attempted if any fail.
158158 * 'nonLocking' : No locks are acquired for the operations.
159159 * This can increase performance for non-critical writes.
160 - * This has no effect unless the 'ignoreErrors' flag is set.
 160+ * This has no effect unless the 'force' flag is set.
161161 * 'allowStale' : Don't require the latest available data.
162162 * This can increase performance for non-critical writes.
163 - * This has no effect unless the 'ignoreErrors' flag is set.
 163+ * This has no effect unless the 'force' flag is set.
164164 *
165165 * Return value:
166166 * This returns a Status, which contains all warnings and fatals that occured
167167 * during the operation. The 'failCount', 'successCount', and 'success' members
168168 * will reflect each operation attempted. The status will be "OK" unless any
169 - * of the operations failed and the 'ignoreErrors' parameter was not set.
 169+ * of the operations failed and the 'force' parameter was not set.
170170 *
171171 * @param $ops Array List of operations to execute in order
172172 * @param $opts Array Batch operation options
@@ -175,7 +175,7 @@
176176 if ( $this->readOnly != '' ) {
177177 return Status::newFatal( 'backend-fail-readonly', $this->name, $this->readOnly );
178178 }
179 - if ( empty( $opts['ignoreErrors'] ) ) { // sanity
 179+ if ( empty( $opts['force'] ) ) { // sanity
180180 unset( $opts['nonLocking'] );
181181 unset( $opts['allowStale'] );
182182 }
@@ -372,6 +372,33 @@
373373 abstract public function getFileContents( array $params );
374374
375375 /**
 376+ * Get the size (bytes) of a file at a storage path in the backend.
 377+ *
 378+ * $params include:
 379+ * src : source storage path
 380+ * latest : use the latest available data
 381+ *
 382+ * @param $params Array
 383+ * @return integer|false Returns false on failure
 384+ */
 385+ abstract public function getFileSize( array $params );
 386+
 387+ /**
 388+ * Get quick information about a file at a storage path in the backend.
 389+ * The result is an associative array that includes:
 390+ * mtime : the last-modified timestamp (TS_MW) or false
 391+ * size : the file size (bytes) or false
 392+ *
 393+ * $params include:
 394+ * src : source storage path
 395+ * latest : use the latest available data
 396+ *
 397+ * @param $params Array
 398+ * @return Array|false Returns false on failure
 399+ */
 400+ abstract public function getFileStat( array $params );
 401+
 402+ /**
376403 * Get a SHA-1 hash of the file at a storage path in the backend.
377404 *
378405 * $params include:
@@ -398,6 +425,7 @@
399426
400427 /**
401428 * Stream the file at a storage path in the backend.
 429+ * If the file does not exists, a 404 error will be given.
402430 * Appropriate HTTP headers (Status, Content-Type, Content-Length)
403431 * must be sent if streaming began, while none should be sent otherwise.
404432 * Implementations should flush the output buffer before sending data.
@@ -807,43 +835,53 @@
808836 * @see FileBackendBase::fileExists()
809837 */
810838 final public function fileExists( array $params ) {
811 - $path = $params['src'];
812 - if ( isset( $this->cache[$path]['exists'] ) ) {
813 - return $this->cache[$path]['exists'];
 839+ return (bool)$this->getFileStat( $params );
 840+ }
 841+
 842+ /**
 843+ * @see FileBackendBase::getFileTimestamp()
 844+ */
 845+ final public function getFileTimestamp( array $params ) {
 846+ $stat = $this->getFileStat( $params );
 847+ if ( $stat ) {
 848+ return $stat['mtime'];
 849+ } else {
 850+ return false;
814851 }
815 - $exists = $this->doFileExists( $params );
816 - if ( $exists ) { // don't cache negatives
817 - $this->trimCache(); // limit memory
818 - $this->cache[$path]['exists'] = $exists;
819 - }
820 - return $exists;
821852 }
822853
823854 /**
824 - * @see FileBackend::fileExists()
 855+ * @see FileBackendBase::getFileSize()
825856 */
826 - abstract protected function doFileExists( array $params );
 857+ final public function getFileSize( array $params ) {
 858+ $stat = $this->getFileStat( $params );
 859+ if ( $stat ) {
 860+ return $stat['size'];
 861+ } else {
 862+ return false;
 863+ }
 864+ }
827865
828866 /**
829 - * @see FileBackendBase::getFileTimestamp()
 867+ * @see FileBackendBase::getFileStat()
830868 */
831 - final public function getFileTimestamp( array $params ) {
 869+ final public function getFileStat( array $params ) {
832870 $path = $params['src'];
833 - if ( isset( $this->cache[$path]['timestamp'] ) ) {
834 - return $this->cache[$path]['timestamp'];
 871+ if ( isset( $this->cache[$path]['stat'] ) ) {
 872+ return $this->cache[$path]['stat'];
835873 }
836 - $timestamp = $this->doGetFileTimestamp( $params );
837 - if ( $timestamp ) { // don't cache negatives
 874+ $stat = $this->doGetFileStat( $params );
 875+ if ( is_array( $stat ) ) { // don't cache negatives
838876 $this->trimCache(); // limit memory
839 - $this->cache[$path]['timestamp'] = $timestamp;
 877+ $this->cache[$path]['stat'] = $stat;
840878 }
841 - return $timestamp;
 879+ return $stat;
842880 }
843881
844882 /**
845 - * @see FileBackend::getFileTimestamp()
 883+ * @see FileBackend::getFileStat()
846884 */
847 - abstract protected function doGetFileTimestamp( array $params );
 885+ abstract protected function doGetFileStat( array $params );
848886
849887 /**
850888 * @see FileBackendBase::getFileContents()
@@ -862,7 +900,7 @@
863901 /**
864902 * @see FileBackendBase::getFileSha1Base36()
865903 */
866 - public function getFileSha1Base36( array $params ) {
 904+ final public function getFileSha1Base36( array $params ) {
867905 $path = $params['src'];
868906 if ( isset( $this->cache[$path]['sha1'] ) ) {
869907 return $this->cache[$path]['sha1'];
@@ -918,23 +956,39 @@
919957 /**
920958 * @see FileBackendBase::streamFile()
921959 */
922 - public function streamFile( array $params ) {
 960+ final public function streamFile( array $params ) {
923961 $status = Status::newGood();
924962
925 - $fsFile = $this->getLocalReference( $params );
926 - if ( !$fsFile ) {
 963+ $info = $this->getFileStat( $params );
 964+ if ( !$info ) { // let StreamFile handle the 404
 965+ $status->fatal( 'backend-fail-notexists', $params['src'] );
 966+ }
 967+
 968+ // Set output buffer and HTTP headers for stream
 969+ $extraHeaders = $params['headers'] ? $params['headers'] : array();
 970+ $res = StreamFile::prepareForStream( $params['src'], $info, $extraHeaders );
 971+ if ( $res == StreamFile::NOT_MODIFIED ) {
 972+ // do nothing; client cache is up to date
 973+ } elseif ( $res == StreamFile::READY_STREAM ) {
 974+ $status = $this->doStreamFile( $params );
 975+ } else {
927976 $status->fatal( 'backend-fail-stream', $params['src'] );
928 - return $status;
929977 }
930978
931 - $extraHeaders = isset( $params['headers'] )
932 - ? $params['headers']
933 - : array();
 979+ return $status;
 980+ }
934981
935 - $ok = StreamFile::stream( $fsFile->getPath(), $extraHeaders, false );
936 - if ( !$ok ) {
 982+ /**
 983+ * @see FileBackend::streamFile()
 984+ */
 985+ protected function doStreamFile( array $params ) {
 986+ $status = Status::newGood();
 987+
 988+ $fsFile = $this->getLocalReference( $params );
 989+ if ( !$fsFile ) {
937990 $status->fatal( 'backend-fail-stream', $params['src'] );
938 - return $status;
 991+ } elseif ( !readfile( $fsFile->getPath() ) ) {
 992+ $status->fatal( 'backend-fail-stream', $params['src'] );
939993 }
940994
941995 return $status;
Index: trunk/phase3/includes/filerepo/FileRepo.php
@@ -695,7 +695,7 @@
696696 }
697697
698698 // Execute the store operation for each triplet
699 - $opts = array( 'ignoreErrors' => true );
 699+ $opts = array( 'force' => true );
700700 if ( $flags & self::SKIP_LOCKING ) {
701701 $opts['nonLocking'] = true;
702702 }
@@ -750,7 +750,7 @@
751751 }
752752 }
753753 // Actually delete files from storage...
754 - $opts = array( 'ignoreErrors' => true );
 754+ $opts = array( 'force' => true );
755755 $this->backend->doOperations( $operations, $opts );
756756 // Cleanup for disk source files...
757757 foreach ( $sourceFSFilesToDelete as $file ) {
@@ -814,7 +814,7 @@
815815
816816 // Delete the sources if required
817817 if ( $deleteOperations ) {
818 - $opts = array( 'ignoreErrors' => true );
 818+ $opts = array( 'force' => true );
819819 $status->merge( $this->backend->doOperations( $deleteOperations, $opts ) );
820820 }
821821
@@ -967,7 +967,7 @@
968968 }
969969
970970 // Execute the operations for each triplet
971 - $opts = array( 'ignoreErrors' => true );
 971+ $opts = array( 'force' => true );
972972 $status->merge( $backend->doOperations( $operations, $opts ) );
973973 // Cleanup for disk source files...
974974 foreach ( $sourceFSFilesToDelete as $file ) {
@@ -1102,7 +1102,7 @@
11031103 // Move the files by execute the operations for each pair.
11041104 // We're now committed to returning an OK result, which will
11051105 // lead to the files being moved in the DB also.
1106 - $opts = array( 'ignoreErrors' => true );
 1106+ $opts = array( 'force' => true );
11071107 $status->merge( $backend->doOperations( $operations, $opts ) );
11081108
11091109 return $status;
Index: trunk/phase3/includes/StreamFile.php
@@ -5,6 +5,9 @@
66 * @file
77 */
88 class StreamFile {
 9+ const READY_STREAM = 1;
 10+ const NOT_MODIFIED = 2;
 11+
912 /**
1013 * Stream a file to the browser, adding all the headings and fun stuff.
1114 * Headers sent include: Content-type, Content-Length, Last-Modified,
@@ -16,17 +19,44 @@
1720 * @return bool Success
1821 */
1922 public static function stream( $fname, $headers = array(), $sendErrors = true ) {
20 - global $wgLanguageCode;
21 -
2223 wfSuppressWarnings();
2324 $stat = stat( $fname );
2425 wfRestoreWarnings();
25 - if ( !$stat ) {
 26+
 27+ $res = self::prepareForStream( $fname, $stat, $headers, $sendErrors );
 28+ if ( $res == self::NOT_MODIFIED ) {
 29+ return true; // use client cache
 30+ } elseif ( $res == self::READY_STREAM ) {
 31+ return readfile( $fname );
 32+ } else {
 33+ return false; // failed
 34+ }
 35+ }
 36+
 37+ /**
 38+ * Call this function used in preparation before streaming a file.
 39+ * This function does the following:
 40+ * (a) sends Last-Modified, Content-type, and Content-Disposition headers
 41+ * (b) cancels any PHP output buffering and automatic gzipping of output
 42+ * (c) sends Content-Length header based on HTTP_IF_MODIFIED_SINCE check
 43+ *
 44+ * @param $path string Storage path or file system path
 45+ * @param $info Array File stat info with 'mtime' and 'size' fields
 46+ * @param $headers Array Additional headers to send
 47+ * @param $sendErrors bool Send error messages if errors occur (like 404)
 48+ * @return int|false READY_STREAM, NOT_MODIFIED, or false on failure
 49+ */
 50+ public static function prepareForStream(
 51+ $path, array $info, $headers = array(), $sendErrors = true
 52+ ) {
 53+ global $wgLanguageCode;
 54+
 55+ if ( !$info ) {
2656 if ( $sendErrors ) {
2757 header( 'HTTP/1.0 404 Not Found' );
2858 header( 'Cache-Control: no-cache' );
2959 header( 'Content-Type: text/html; charset=utf-8' );
30 - $encFile = htmlspecialchars( $fname );
 60+ $encFile = htmlspecialchars( $path );
3161 $encScript = htmlspecialchars( $_SERVER['SCRIPT_NAME'] );
3262 echo "<html><body>
3363 <h1>File not found</h1>
@@ -38,12 +68,13 @@
3969 return false;
4070 }
4171
42 - header( 'Last-Modified: ' . gmdate( 'D, d M Y H:i:s', $stat['mtime'] ) . ' GMT' );
 72+ // Sent Last-Modified HTTP header for client-side caching
 73+ header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $info['mtime'] ) );
4374
4475 // Cancel output buffering and gzipping if set
4576 wfResetOutputBuffers();
4677
47 - $type = self::getType( $fname );
 78+ $type = self::getType( $path );
4879 if ( $type && $type != 'unknown/unknown' ) {
4980 header( "Content-type: $type" );
5081 } else {
@@ -57,7 +88,7 @@
5889 }
5990
6091 header( "Content-Disposition: inline;filename*=utf-8'$wgLanguageCode'" .
61 - urlencode( basename( $fname ) ) );
 92+ urlencode( basename( $path ) ) );
6293
6394 // Send additional headers
6495 foreach ( $headers as $header ) {
@@ -67,26 +98,25 @@
6899 // Don't send if client has up to date cache
69100 if ( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
70101 $modsince = preg_replace( '/;.*$/', '', $_SERVER['HTTP_IF_MODIFIED_SINCE'] );
71 - $sinceTime = strtotime( $modsince );
72 - if ( $stat['mtime'] <= $sinceTime ) {
 102+ if ( wfTimestamp( TS_UNIX, $info['mtime'] ) <= strtotime( $modsince ) ) {
73103 ini_set( 'zlib.output_compression', 0 );
74104 header( "HTTP/1.0 304 Not Modified" );
75 - return true; // ok
 105+ return self::NOT_MODIFIED; // ok
76106 }
77107 }
78108
79 - header( 'Content-Length: ' . $stat['size'] );
 109+ header( 'Content-Length: ' . $info['size'] );
80110
81 - return readfile( $fname );
 111+ return self::READY_STREAM; // ok
82112 }
83113
84114 /**
85115 * Determine the filetype we're dealing with
86 - * @param $filename string
87 - * @param $safe bool
 116+ * @param $filename string Storage path or file system path
 117+ * @param $safe bool Whether to do retroactive upload blacklist checks
88118 * @return null|string
89119 */
90 - private static function getType( $filename, $safe = true ) {
 120+ protected static function getType( $filename, $safe = true ) {
91121 global $wgTrivialMimeDetection;
92122
93123 $ext = strrchr( $filename, '.' );
Index: trunk/phase3/languages/messages/MessagesEn.php
@@ -2254,6 +2254,7 @@
22552255 'backend-fail-read' => 'Could not read file $1.',
22562256 'backend-fail-create' => 'Could not create file $1.',
22572257 'backend-fail-readonly' => 'The backend "$1" is currently read-only. The reason given is: "$2"',
 2258+'backend-fail-synced' => 'The file "$1" is in an inconsistent state within the internal backends',
22582259
22592260 # Lock manager
22602261 'lockmanager-notlocked' => 'Could not unlock "$1"; it is not locked.',

Follow-up revisions

RevisionCommit summaryAuthorDate
r108481In SwiftFileBackend:...aaron00:24, 10 January 2012
r108488r108353: Distinguish null/false in FileBackend::fileExists(). This is intende...aaron02:18, 10 January 2012
r109106* Fixed type check in StreamFile::prepareForStream() for 404s...aaron05:34, 17 January 2012
r110269* r108353: Made FileBackendMultiWrite consistency checks configurable....aaron08:00, 30 January 2012

Comments

#Comment by Tim Starling (talk | contribs)   01:27, 26 January 2012

The consistency check is concerning because it requires the file timestamps for a given file to match in all backends. During initial setup, this might be difficult to achieve, depending on what clients and script are available for populating the object store with data. Perhaps the backend could have a "consistency check mode" option which would allow the user to choose between:

  • No consistency check
  • Check size (good default?)
  • Check timestamp
  • Both?

Status & tagging log