MediaWiki  master
FileBackendTest.php
Go to the documentation of this file.
1 <?php
2 
4 
49 
51  private $backend;
53  private $multiBackend;
56  private static $backendToUse;
57 
58  protected function setUp() {
59  global $wgFileBackends;
60  parent::setUp();
61  $tmpDir = $this->getNewTempDirectory();
62  if ( $this->getCliArg( 'use-filebackend' ) ) {
63  if ( self::$backendToUse ) {
64  $this->singleBackend = self::$backendToUse;
65  } else {
66  $name = $this->getCliArg( 'use-filebackend' );
67  $useConfig = [];
68  foreach ( $wgFileBackends as $conf ) {
69  if ( $conf['name'] == $name ) {
70  $useConfig = $conf;
71  break;
72  }
73  }
74  $useConfig['name'] = 'localtesting'; // swap name
75  $useConfig['shardViaHashLevels'] = [ // test sharding
76  'unittest-cont1' => [ 'levels' => 1, 'base' => 16, 'repeat' => 1 ]
77  ];
78  if ( isset( $useConfig['fileJournal'] ) ) {
79  $useConfig['fileJournal'] = FileJournal::factory( $useConfig['fileJournal'], $name );
80  }
81  $useConfig['lockManager'] = LockManagerGroup::singleton()->get( $useConfig['lockManager'] );
82  $class = $useConfig['class'];
83  self::$backendToUse = new $class( $useConfig );
84  $this->singleBackend = self::$backendToUse;
85  }
86  } else {
87  $this->singleBackend = new FSFileBackend( [
88  'name' => 'localtesting',
89  'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ),
90  'wikiId' => wfWikiID(),
91  'containerPaths' => [
92  'unittest-cont1' => "{$tmpDir}/localtesting-cont1",
93  'unittest-cont2' => "{$tmpDir}/localtesting-cont2" ]
94  ] );
95  }
96  $this->multiBackend = new FileBackendMultiWrite( [
97  'name' => 'localtesting',
98  'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ),
99  'parallelize' => 'implicit',
100  'wikiId' => wfWikiID() . wfRandomString(),
101  'backends' => [
102  [
103  'name' => 'localmultitesting1',
104  'class' => FSFileBackend::class,
105  'containerPaths' => [
106  'unittest-cont1' => "{$tmpDir}/localtestingmulti1-cont1",
107  'unittest-cont2' => "{$tmpDir}/localtestingmulti1-cont2" ],
108  'isMultiMaster' => false
109  ],
110  [
111  'name' => 'localmultitesting2',
112  'class' => FSFileBackend::class,
113  'containerPaths' => [
114  'unittest-cont1' => "{$tmpDir}/localtestingmulti2-cont1",
115  'unittest-cont2' => "{$tmpDir}/localtestingmulti2-cont2" ],
116  'isMultiMaster' => true
117  ]
118  ]
119  ] );
120  }
121 
122  private static function baseStorePath() {
123  return 'mwstore://localtesting';
124  }
125 
126  private function backendClass() {
127  return get_class( $this->backend );
128  }
129 
133  public function testIsStoragePath( $path, $isStorePath ) {
134  $this->assertEquals( $isStorePath, FileBackend::isStoragePath( $path ),
135  "FileBackend::isStoragePath on path '$path'" );
136  }
137 
138  public static function provider_testIsStoragePath() {
139  return [
140  [ 'mwstore://', true ],
141  [ 'mwstore://backend', true ],
142  [ 'mwstore://backend/container', true ],
143  [ 'mwstore://backend/container/', true ],
144  [ 'mwstore://backend/container/path', true ],
145  [ 'mwstore://backend//container/', true ],
146  [ 'mwstore://backend//container//', true ],
147  [ 'mwstore://backend//container//path', true ],
148  [ 'mwstore:///', true ],
149  [ 'mwstore:/', false ],
150  [ 'mwstore:', false ],
151  ];
152  }
153 
157  public function testSplitStoragePath( $path, $res ) {
158  $this->assertEquals( $res, FileBackend::splitStoragePath( $path ),
159  "FileBackend::splitStoragePath on path '$path'" );
160  }
161 
162  public static function provider_testSplitStoragePath() {
163  return [
164  [ 'mwstore://backend/container', [ 'backend', 'container', '' ] ],
165  [ 'mwstore://backend/container/', [ 'backend', 'container', '' ] ],
166  [ 'mwstore://backend/container/path', [ 'backend', 'container', 'path' ] ],
167  [ 'mwstore://backend/container//path', [ 'backend', 'container', '/path' ] ],
168  [ 'mwstore://backend//container/path', [ null, null, null ] ],
169  [ 'mwstore://backend//container//path', [ null, null, null ] ],
170  [ 'mwstore://', [ null, null, null ] ],
171  [ 'mwstore://backend', [ null, null, null ] ],
172  [ 'mwstore:///', [ null, null, null ] ],
173  [ 'mwstore:/', [ null, null, null ] ],
174  [ 'mwstore:', [ null, null, null ] ]
175  ];
176  }
177 
181  public function testNormalizeStoragePath( $path, $res ) {
182  $this->assertEquals( $res, FileBackend::normalizeStoragePath( $path ),
183  "FileBackend::normalizeStoragePath on path '$path'" );
184  }
185 
186  public static function provider_normalizeStoragePath() {
187  return [
188  [ 'mwstore://backend/container', 'mwstore://backend/container' ],
189  [ 'mwstore://backend/container/', 'mwstore://backend/container' ],
190  [ 'mwstore://backend/container/path', 'mwstore://backend/container/path' ],
191  [ 'mwstore://backend/container//path', 'mwstore://backend/container/path' ],
192  [ 'mwstore://backend/container///path', 'mwstore://backend/container/path' ],
193  [
194  'mwstore://backend/container///path//to///obj',
195  'mwstore://backend/container/path/to/obj'
196  ],
197  [ 'mwstore://', null ],
198  [ 'mwstore://backend', null ],
199  [ 'mwstore://backend//container/path', null ],
200  [ 'mwstore://backend//container//path', null ],
201  [ 'mwstore:///', null ],
202  [ 'mwstore:/', null ],
203  [ 'mwstore:', null ],
204  ];
205  }
206 
210  public function testParentStoragePath( $path, $res ) {
211  $this->assertEquals( $res, FileBackend::parentStoragePath( $path ),
212  "FileBackend::parentStoragePath on path '$path'" );
213  }
214 
215  public static function provider_testParentStoragePath() {
216  return [
217  [ 'mwstore://backend/container/path/to/obj', 'mwstore://backend/container/path/to' ],
218  [ 'mwstore://backend/container/path/to', 'mwstore://backend/container/path' ],
219  [ 'mwstore://backend/container/path', 'mwstore://backend/container' ],
220  [ 'mwstore://backend/container', null ],
221  [ 'mwstore://backend/container/path/to/obj/', 'mwstore://backend/container/path/to' ],
222  [ 'mwstore://backend/container/path/to/', 'mwstore://backend/container/path' ],
223  [ 'mwstore://backend/container/path/', 'mwstore://backend/container' ],
224  [ 'mwstore://backend/container/', null ],
225  ];
226  }
227 
231  public function testExtensionFromPath( $path, $res ) {
232  $this->assertEquals( $res, FileBackend::extensionFromPath( $path ),
233  "FileBackend::extensionFromPath on path '$path'" );
234  }
235 
236  public static function provider_testExtensionFromPath() {
237  return [
238  [ 'mwstore://backend/container/path.txt', 'txt' ],
239  [ 'mwstore://backend/container/path.svg.png', 'png' ],
240  [ 'mwstore://backend/container/path', '' ],
241  [ 'mwstore://backend/container/path.', '' ],
242  ];
243  }
244 
248  public function testStore( $op ) {
249  $this->addTmpFiles( $op['src'] );
250 
251  $this->backend = $this->singleBackend;
252  $this->tearDownFiles();
253  $this->doTestStore( $op );
254  $this->tearDownFiles();
255 
256  $this->backend = $this->multiBackend;
257  $this->tearDownFiles();
258  $this->doTestStore( $op );
259  $this->tearDownFiles();
260  }
261 
262  private function doTestStore( $op ) {
263  $backendName = $this->backendClass();
264 
265  $source = $op['src'];
266  $dest = $op['dst'];
267  $this->prepare( [ 'dir' => dirname( $dest ) ] );
268 
269  file_put_contents( $source, "Unit test file" );
270 
271  if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
272  $this->backend->store( $op );
273  }
274 
275  $status = $this->backend->doOperation( $op );
276 
277  $this->assertGoodStatus( $status,
278  "Store from $source to $dest succeeded without warnings ($backendName)." );
279  $this->assertEquals( true, $status->isOK(),
280  "Store from $source to $dest succeeded ($backendName)." );
281  $this->assertEquals( [ 0 => true ], $status->success,
282  "Store from $source to $dest has proper 'success' field in Status ($backendName)." );
283  $this->assertEquals( true, file_exists( $source ),
284  "Source file $source still exists ($backendName)." );
285  $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ),
286  "Destination file $dest exists ($backendName)." );
287 
288  $this->assertEquals( filesize( $source ),
289  $this->backend->getFileSize( [ 'src' => $dest ] ),
290  "Destination file $dest has correct size ($backendName)." );
291 
292  $props1 = FSFile::getPropsFromPath( $source );
293  $props2 = $this->backend->getFileProps( [ 'src' => $dest ] );
294  $this->assertEquals( $props1, $props2,
295  "Source and destination have the same props ($backendName)." );
296 
297  $this->assertBackendPathsConsistent( [ $dest ] );
298  }
299 
300  public static function provider_testStore() {
301  $cases = [];
302 
303  $tmpName = TempFSFile::factory( "unittests_", 'txt', wfTempDir() )->getPath();
304  $toPath = self::baseStorePath() . '/unittest-cont1/e/fun/obj1.txt';
305  $op = [ 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath ];
306  $cases[] = [ $op ];
307 
308  $op2 = $op;
309  $op2['overwrite'] = true;
310  $cases[] = [ $op2 ];
311 
312  $op3 = $op;
313  $op3['overwriteSame'] = true;
314  $cases[] = [ $op3 ];
315 
316  return $cases;
317  }
318 
322  public function testCopy( $op ) {
323  $this->backend = $this->singleBackend;
324  $this->tearDownFiles();
325  $this->doTestCopy( $op );
326  $this->tearDownFiles();
327 
328  $this->backend = $this->multiBackend;
329  $this->tearDownFiles();
330  $this->doTestCopy( $op );
331  $this->tearDownFiles();
332  }
333 
334  private function doTestCopy( $op ) {
335  $backendName = $this->backendClass();
336 
337  $source = $op['src'];
338  $dest = $op['dst'];
339  $this->prepare( [ 'dir' => dirname( $source ) ] );
340  $this->prepare( [ 'dir' => dirname( $dest ) ] );
341 
342  if ( isset( $op['ignoreMissingSource'] ) ) {
343  $status = $this->backend->doOperation( $op );
344  $this->assertGoodStatus( $status,
345  "Move from $source to $dest succeeded without warnings ($backendName)." );
346  $this->assertEquals( [ 0 => true ], $status->success,
347  "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
348  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
349  "Source file $source does not exist ($backendName)." );
350  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $dest ] ),
351  "Destination file $dest does not exist ($backendName)." );
352 
353  return;
354  }
355 
356  $status = $this->backend->doOperation(
357  [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] );
358  $this->assertGoodStatus( $status,
359  "Creation of file at $source succeeded ($backendName)." );
360 
361  if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
362  $this->backend->copy( $op );
363  }
364 
365  $status = $this->backend->doOperation( $op );
366 
367  $this->assertGoodStatus( $status,
368  "Copy from $source to $dest succeeded without warnings ($backendName)." );
369  $this->assertEquals( true, $status->isOK(),
370  "Copy from $source to $dest succeeded ($backendName)." );
371  $this->assertEquals( [ 0 => true ], $status->success,
372  "Copy from $source to $dest has proper 'success' field in Status ($backendName)." );
373  $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $source ] ),
374  "Source file $source still exists ($backendName)." );
375  $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ),
376  "Destination file $dest exists after copy ($backendName)." );
377 
378  $this->assertEquals(
379  $this->backend->getFileSize( [ 'src' => $source ] ),
380  $this->backend->getFileSize( [ 'src' => $dest ] ),
381  "Destination file $dest has correct size ($backendName)." );
382 
383  $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
384  $props2 = $this->backend->getFileProps( [ 'src' => $dest ] );
385  $this->assertEquals( $props1, $props2,
386  "Source and destination have the same props ($backendName)." );
387 
388  $this->assertBackendPathsConsistent( [ $source, $dest ] );
389  }
390 
391  public static function provider_testCopy() {
392  $cases = [];
393 
394  $source = self::baseStorePath() . '/unittest-cont1/e/file.txt';
395  $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
396 
397  $op = [ 'op' => 'copy', 'src' => $source, 'dst' => $dest ];
398  $cases[] = [
399  $op, // operation
400  $source, // source
401  $dest, // dest
402  ];
403 
404  $op2 = $op;
405  $op2['overwrite'] = true;
406  $cases[] = [
407  $op2, // operation
408  $source, // source
409  $dest, // dest
410  ];
411 
412  $op2 = $op;
413  $op2['overwriteSame'] = true;
414  $cases[] = [
415  $op2, // operation
416  $source, // source
417  $dest, // dest
418  ];
419 
420  $op2 = $op;
421  $op2['ignoreMissingSource'] = true;
422  $cases[] = [
423  $op2, // operation
424  $source, // source
425  $dest, // dest
426  ];
427 
428  $op2 = $op;
429  $op2['ignoreMissingSource'] = true;
430  $cases[] = [
431  $op2, // operation
432  self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source
433  $dest, // dest
434  ];
435 
436  return $cases;
437  }
438 
442  public function testMove( $op ) {
443  $this->backend = $this->singleBackend;
444  $this->tearDownFiles();
445  $this->doTestMove( $op );
446  $this->tearDownFiles();
447 
448  $this->backend = $this->multiBackend;
449  $this->tearDownFiles();
450  $this->doTestMove( $op );
451  $this->tearDownFiles();
452  }
453 
454  private function doTestMove( $op ) {
455  $backendName = $this->backendClass();
456 
457  $source = $op['src'];
458  $dest = $op['dst'];
459  $this->prepare( [ 'dir' => dirname( $source ) ] );
460  $this->prepare( [ 'dir' => dirname( $dest ) ] );
461 
462  if ( isset( $op['ignoreMissingSource'] ) ) {
463  $status = $this->backend->doOperation( $op );
464  $this->assertGoodStatus( $status,
465  "Move from $source to $dest succeeded without warnings ($backendName)." );
466  $this->assertEquals( [ 0 => true ], $status->success,
467  "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
468  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
469  "Source file $source does not exist ($backendName)." );
470  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $dest ] ),
471  "Destination file $dest does not exist ($backendName)." );
472 
473  return;
474  }
475 
476  $status = $this->backend->doOperation(
477  [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] );
478  $this->assertGoodStatus( $status,
479  "Creation of file at $source succeeded ($backendName)." );
480 
481  if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
482  $this->backend->copy( $op );
483  }
484 
485  $status = $this->backend->doOperation( $op );
486  $this->assertGoodStatus( $status,
487  "Move from $source to $dest succeeded without warnings ($backendName)." );
488  $this->assertEquals( true, $status->isOK(),
489  "Move from $source to $dest succeeded ($backendName)." );
490  $this->assertEquals( [ 0 => true ], $status->success,
491  "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
492  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
493  "Source file $source does not still exists ($backendName)." );
494  $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ),
495  "Destination file $dest exists after move ($backendName)." );
496 
497  $this->assertNotEquals(
498  $this->backend->getFileSize( [ 'src' => $source ] ),
499  $this->backend->getFileSize( [ 'src' => $dest ] ),
500  "Destination file $dest has correct size ($backendName)." );
501 
502  $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
503  $props2 = $this->backend->getFileProps( [ 'src' => $dest ] );
504  $this->assertEquals( false, $props1['fileExists'],
505  "Source file does not exist accourding to props ($backendName)." );
506  $this->assertEquals( true, $props2['fileExists'],
507  "Destination file exists accourding to props ($backendName)." );
508 
509  $this->assertBackendPathsConsistent( [ $source, $dest ] );
510  }
511 
512  public static function provider_testMove() {
513  $cases = [];
514 
515  $source = self::baseStorePath() . '/unittest-cont1/e/file.txt';
516  $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt';
517 
518  $op = [ 'op' => 'move', 'src' => $source, 'dst' => $dest ];
519  $cases[] = [
520  $op, // operation
521  $source, // source
522  $dest, // dest
523  ];
524 
525  $op2 = $op;
526  $op2['overwrite'] = true;
527  $cases[] = [
528  $op2, // operation
529  $source, // source
530  $dest, // dest
531  ];
532 
533  $op2 = $op;
534  $op2['overwriteSame'] = true;
535  $cases[] = [
536  $op2, // operation
537  $source, // source
538  $dest, // dest
539  ];
540 
541  $op2 = $op;
542  $op2['ignoreMissingSource'] = true;
543  $cases[] = [
544  $op2, // operation
545  $source, // source
546  $dest, // dest
547  ];
548 
549  $op2 = $op;
550  $op2['ignoreMissingSource'] = true;
551  $cases[] = [
552  $op2, // operation
553  self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source
554  $dest, // dest
555  ];
556 
557  return $cases;
558  }
559 
563  public function testDelete( $op, $withSource, $okStatus ) {
564  $this->backend = $this->singleBackend;
565  $this->tearDownFiles();
566  $this->doTestDelete( $op, $withSource, $okStatus );
567  $this->tearDownFiles();
568 
569  $this->backend = $this->multiBackend;
570  $this->tearDownFiles();
571  $this->doTestDelete( $op, $withSource, $okStatus );
572  $this->tearDownFiles();
573  }
574 
575  private function doTestDelete( $op, $withSource, $okStatus ) {
576  $backendName = $this->backendClass();
577 
578  $source = $op['src'];
579  $this->prepare( [ 'dir' => dirname( $source ) ] );
580 
581  if ( $withSource ) {
582  $status = $this->backend->doOperation(
583  [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] );
584  $this->assertGoodStatus( $status,
585  "Creation of file at $source succeeded ($backendName)." );
586  }
587 
588  $status = $this->backend->doOperation( $op );
589  if ( $okStatus ) {
590  $this->assertGoodStatus( $status,
591  "Deletion of file at $source succeeded without warnings ($backendName)." );
592  $this->assertEquals( true, $status->isOK(),
593  "Deletion of file at $source succeeded ($backendName)." );
594  $this->assertEquals( [ 0 => true ], $status->success,
595  "Deletion of file at $source has proper 'success' field in Status ($backendName)." );
596  } else {
597  $this->assertEquals( false, $status->isOK(),
598  "Deletion of file at $source failed ($backendName)." );
599  }
600 
601  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ),
602  "Source file $source does not exist after move ($backendName)." );
603 
604  $this->assertFalse(
605  $this->backend->getFileSize( [ 'src' => $source ] ),
606  "Source file $source has correct size (false) ($backendName)." );
607 
608  $props1 = $this->backend->getFileProps( [ 'src' => $source ] );
609  $this->assertFalse( $props1['fileExists'],
610  "Source file $source does not exist according to props ($backendName)." );
611 
613  }
614 
615  public static function provider_testDelete() {
616  $cases = [];
617 
618  $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt';
619 
620  $op = [ 'op' => 'delete', 'src' => $source ];
621  $cases[] = [
622  $op, // operation
623  true, // with source
624  true // succeeds
625  ];
626 
627  $cases[] = [
628  $op, // operation
629  false, // without source
630  false // fails
631  ];
632 
633  $op['ignoreMissingSource'] = true;
634  $cases[] = [
635  $op, // operation
636  false, // without source
637  true // succeeds
638  ];
639 
640  $op['ignoreMissingSource'] = true;
641  $op['src'] = self::baseStorePath() . '/unittest-cont-bad/e/file.txt';
642  $cases[] = [
643  $op, // operation
644  false, // without source
645  true // succeeds
646  ];
647 
648  return $cases;
649  }
650 
654  public function testDescribe( $op, $withSource, $okStatus ) {
655  $this->backend = $this->singleBackend;
656  $this->tearDownFiles();
657  $this->doTestDescribe( $op, $withSource, $okStatus );
658  $this->tearDownFiles();
659 
660  $this->backend = $this->multiBackend;
661  $this->tearDownFiles();
662  $this->doTestDescribe( $op, $withSource, $okStatus );
663  $this->tearDownFiles();
664  }
665 
666  private function doTestDescribe( $op, $withSource, $okStatus ) {
667  $backendName = $this->backendClass();
668 
669  $source = $op['src'];
670  $this->prepare( [ 'dir' => dirname( $source ) ] );
671 
672  if ( $withSource ) {
673  $status = $this->backend->doOperation(
674  [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source,
675  'headers' => [ 'Content-Disposition' => 'xxx' ] ] );
676  $this->assertGoodStatus( $status,
677  "Creation of file at $source succeeded ($backendName)." );
678  if ( $this->backend->hasFeatures( FileBackend::ATTR_HEADERS ) ) {
679  $attr = $this->backend->getFileXAttributes( [ 'src' => $source ] );
680  $this->assertHasHeaders( [ 'Content-Disposition' => 'xxx' ], $attr );
681  }
682 
683  $status = $this->backend->describe( [ 'src' => $source,
684  'headers' => [ 'Content-Disposition' => '' ] ] ); // remove
685  $this->assertGoodStatus( $status,
686  "Removal of header for $source succeeded ($backendName)." );
687 
688  if ( $this->backend->hasFeatures( FileBackend::ATTR_HEADERS ) ) {
689  $attr = $this->backend->getFileXAttributes( [ 'src' => $source ] );
690  $this->assertFalse( isset( $attr['headers']['content-disposition'] ),
691  "File 'Content-Disposition' header removed." );
692  }
693  }
694 
695  $status = $this->backend->doOperation( $op );
696  if ( $okStatus ) {
697  $this->assertGoodStatus( $status,
698  "Describe of file at $source succeeded without warnings ($backendName)." );
699  $this->assertEquals( true, $status->isOK(),
700  "Describe of file at $source succeeded ($backendName)." );
701  $this->assertEquals( [ 0 => true ], $status->success,
702  "Describe of file at $source has proper 'success' field in Status ($backendName)." );
703  if ( $this->backend->hasFeatures( FileBackend::ATTR_HEADERS ) ) {
704  $attr = $this->backend->getFileXAttributes( [ 'src' => $source ] );
705  $this->assertHasHeaders( $op['headers'], $attr );
706  }
707  } else {
708  $this->assertEquals( false, $status->isOK(),
709  "Describe of file at $source failed ($backendName)." );
710  }
711 
713  }
714 
715  private function assertHasHeaders( array $headers, array $attr ) {
716  foreach ( $headers as $n => $v ) {
717  if ( $n !== '' ) {
718  $this->assertTrue( isset( $attr['headers'][strtolower( $n )] ),
719  "File has '$n' header." );
720  $this->assertEquals( $v, $attr['headers'][strtolower( $n )],
721  "File has '$n' header value." );
722  } else {
723  $this->assertFalse( isset( $attr['headers'][strtolower( $n )] ),
724  "File does not have '$n' header." );
725  }
726  }
727  }
728 
729  public static function provider_testDescribe() {
730  $cases = [];
731 
732  $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt';
733 
734  $op = [ 'op' => 'describe', 'src' => $source,
735  'headers' => [ 'Content-Disposition' => 'inline' ], ];
736  $cases[] = [
737  $op, // operation
738  true, // with source
739  true // succeeds
740  ];
741 
742  $cases[] = [
743  $op, // operation
744  false, // without source
745  false // fails
746  ];
747 
748  return $cases;
749  }
750 
754  public function testCreate( $op, $alreadyExists, $okStatus, $newSize ) {
755  $this->backend = $this->singleBackend;
756  $this->tearDownFiles();
757  $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize );
758  $this->tearDownFiles();
759 
760  $this->backend = $this->multiBackend;
761  $this->tearDownFiles();
762  $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize );
763  $this->tearDownFiles();
764  }
765 
766  private function doTestCreate( $op, $alreadyExists, $okStatus, $newSize ) {
767  $backendName = $this->backendClass();
768 
769  $dest = $op['dst'];
770  $this->prepare( [ 'dir' => dirname( $dest ) ] );
771 
772  $oldText = 'blah...blah...waahwaah';
773  if ( $alreadyExists ) {
774  $status = $this->backend->doOperation(
775  [ 'op' => 'create', 'content' => $oldText, 'dst' => $dest ] );
776  $this->assertGoodStatus( $status,
777  "Creation of file at $dest succeeded ($backendName)." );
778  }
779 
780  $status = $this->backend->doOperation( $op );
781  if ( $okStatus ) {
782  $this->assertGoodStatus( $status,
783  "Creation of file at $dest succeeded without warnings ($backendName)." );
784  $this->assertEquals( true, $status->isOK(),
785  "Creation of file at $dest succeeded ($backendName)." );
786  $this->assertEquals( [ 0 => true ], $status->success,
787  "Creation of file at $dest has proper 'success' field in Status ($backendName)." );
788  } else {
789  $this->assertEquals( false, $status->isOK(),
790  "Creation of file at $dest failed ($backendName)." );
791  }
792 
793  $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ),
794  "Destination file $dest exists after creation ($backendName)." );
795 
796  $props1 = $this->backend->getFileProps( [ 'src' => $dest ] );
797  $this->assertEquals( true, $props1['fileExists'],
798  "Destination file $dest exists according to props ($backendName)." );
799  if ( $okStatus ) { // file content is what we saved
800  $this->assertEquals( $newSize, $props1['size'],
801  "Destination file $dest has expected size according to props ($backendName)." );
802  $this->assertEquals( $newSize,
803  $this->backend->getFileSize( [ 'src' => $dest ] ),
804  "Destination file $dest has correct size ($backendName)." );
805  } else { // file content is some other previous text
806  $this->assertEquals( strlen( $oldText ), $props1['size'],
807  "Destination file $dest has original size according to props ($backendName)." );
808  $this->assertEquals( strlen( $oldText ),
809  $this->backend->getFileSize( [ 'src' => $dest ] ),
810  "Destination file $dest has original size according to props ($backendName)." );
811  }
812 
813  $this->assertBackendPathsConsistent( [ $dest ] );
814  }
815 
819  public static function provider_testCreate() {
820  $cases = [];
821 
822  $dest = self::baseStorePath() . '/unittest-cont2/a/myspacefile.txt';
823 
824  $op = [ 'op' => 'create', 'content' => 'test test testing', 'dst' => $dest ];
825  $cases[] = [
826  $op, // operation
827  false, // no dest already exists
828  true, // succeeds
829  strlen( $op['content'] )
830  ];
831 
832  $op2 = $op;
833  $op2['content'] = "\n";
834  $cases[] = [
835  $op2, // operation
836  false, // no dest already exists
837  true, // succeeds
838  strlen( $op2['content'] )
839  ];
840 
841  $op2 = $op;
842  $op2['content'] = "fsf\n waf 3kt";
843  $cases[] = [
844  $op2, // operation
845  true, // dest already exists
846  false, // fails
847  strlen( $op2['content'] )
848  ];
849 
850  $op2 = $op;
851  $op2['content'] = "egm'g gkpe gpqg eqwgwqg";
852  $op2['overwrite'] = true;
853  $cases[] = [
854  $op2, // operation
855  true, // dest already exists
856  true, // succeeds
857  strlen( $op2['content'] )
858  ];
859 
860  $op2 = $op;
861  $op2['content'] = "39qjmg3-qg";
862  $op2['overwriteSame'] = true;
863  $cases[] = [
864  $op2, // operation
865  true, // dest already exists
866  false, // succeeds
867  strlen( $op2['content'] )
868  ];
869 
870  return $cases;
871  }
872 
873  public function testDoQuickOperations() {
874  $this->backend = $this->singleBackend;
875  $this->doTestDoQuickOperations();
876  $this->tearDownFiles();
877 
878  $this->backend = $this->multiBackend;
879  $this->doTestDoQuickOperations();
880  $this->tearDownFiles();
881  }
882 
883  private function doTestDoQuickOperations() {
884  $backendName = $this->backendClass();
885 
886  $base = self::baseStorePath();
887  $files = [
888  "$base/unittest-cont1/e/fileA.a",
889  "$base/unittest-cont1/e/fileB.a",
890  "$base/unittest-cont1/e/fileC.a"
891  ];
892  $createOps = [];
893  $purgeOps = [];
894  foreach ( $files as $path ) {
895  $status = $this->prepare( [ 'dir' => dirname( $path ) ] );
896  $this->assertGoodStatus( $status,
897  "Preparing $path succeeded without warnings ($backendName)." );
898  $createOps[] = [ 'op' => 'create', 'dst' => $path, 'content' => mt_rand( 0, 50000 ) ];
899  $copyOps[] = [ 'op' => 'copy', 'src' => $path, 'dst' => "$path-2" ];
900  $moveOps[] = [ 'op' => 'move', 'src' => "$path-2", 'dst' => "$path-3" ];
901  $purgeOps[] = [ 'op' => 'delete', 'src' => $path ];
902  $purgeOps[] = [ 'op' => 'delete', 'src' => "$path-3" ];
903  }
904  $purgeOps[] = [ 'op' => 'null' ];
905 
906  $this->assertGoodStatus(
907  $this->backend->doQuickOperations( $createOps ),
908  "Creation of source files succeeded ($backendName)." );
909  foreach ( $files as $file ) {
910  $this->assertTrue( $this->backend->fileExists( [ 'src' => $file ] ),
911  "File $file exists." );
912  }
913 
914  $this->assertGoodStatus(
915  $this->backend->doQuickOperations( $copyOps ),
916  "Quick copy of source files succeeded ($backendName)." );
917  foreach ( $files as $file ) {
918  $this->assertTrue( $this->backend->fileExists( [ 'src' => "$file-2" ] ),
919  "File $file-2 exists." );
920  }
921 
922  $this->assertGoodStatus(
923  $this->backend->doQuickOperations( $moveOps ),
924  "Quick move of source files succeeded ($backendName)." );
925  foreach ( $files as $file ) {
926  $this->assertTrue( $this->backend->fileExists( [ 'src' => "$file-3" ] ),
927  "File $file-3 move in." );
928  $this->assertFalse( $this->backend->fileExists( [ 'src' => "$file-2" ] ),
929  "File $file-2 moved away." );
930  }
931 
932  $this->assertGoodStatus(
933  $this->backend->quickCopy( [ 'src' => $files[0], 'dst' => $files[0] ] ),
934  "Copy of file {$files[0]} over itself succeeded ($backendName)." );
935  $this->assertTrue( $this->backend->fileExists( [ 'src' => $files[0] ] ),
936  "File {$files[0]} still exists." );
937 
938  $this->assertGoodStatus(
939  $this->backend->quickMove( [ 'src' => $files[0], 'dst' => $files[0] ] ),
940  "Move of file {$files[0]} over itself succeeded ($backendName)." );
941  $this->assertTrue( $this->backend->fileExists( [ 'src' => $files[0] ] ),
942  "File {$files[0]} still exists." );
943 
944  $this->assertGoodStatus(
945  $this->backend->doQuickOperations( $purgeOps ),
946  "Quick deletion of source files succeeded ($backendName)." );
947  foreach ( $files as $file ) {
948  $this->assertFalse( $this->backend->fileExists( [ 'src' => $file ] ),
949  "File $file purged." );
950  $this->assertFalse( $this->backend->fileExists( [ 'src' => "$file-3" ] ),
951  "File $file-3 purged." );
952  }
953  }
954 
958  public function testConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
959  $this->backend = $this->singleBackend;
960  $this->tearDownFiles();
961  $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
962  $this->tearDownFiles();
963 
964  $this->backend = $this->multiBackend;
965  $this->tearDownFiles();
966  $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
967  $this->tearDownFiles();
968  }
969 
970  private function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
971  $backendName = $this->backendClass();
972 
973  $expContent = '';
974  // Create sources
975  $ops = [];
976  foreach ( $srcs as $i => $source ) {
977  $this->prepare( [ 'dir' => dirname( $source ) ] );
978  $ops[] = [
979  'op' => 'create', // operation
980  'dst' => $source, // source
981  'content' => $srcsContent[$i]
982  ];
983  $expContent .= $srcsContent[$i];
984  }
985  $status = $this->backend->doOperations( $ops );
986 
987  $this->assertGoodStatus( $status,
988  "Creation of source files succeeded ($backendName)." );
989 
990  $dest = $params['dst'] = $this->getNewTempFile();
991  if ( $alreadyExists ) {
992  $ok = file_put_contents( $dest, 'blah...blah...waahwaah' ) !== false;
993  $this->assertEquals( true, $ok,
994  "Creation of file at $dest succeeded ($backendName)." );
995  } else {
996  $ok = file_put_contents( $dest, '' ) !== false;
997  $this->assertEquals( true, $ok,
998  "Creation of 0-byte file at $dest succeeded ($backendName)." );
999  }
1000 
1001  // Combine the files into one
1002  $status = $this->backend->concatenate( $params );
1003  if ( $okStatus ) {
1004  $this->assertGoodStatus( $status,
1005  "Creation of concat file at $dest succeeded without warnings ($backendName)." );
1006  $this->assertEquals( true, $status->isOK(),
1007  "Creation of concat file at $dest succeeded ($backendName)." );
1008  } else {
1009  $this->assertEquals( false, $status->isOK(),
1010  "Creation of concat file at $dest failed ($backendName)." );
1011  }
1012 
1013  if ( $okStatus ) {
1014  $this->assertEquals( true, is_file( $dest ),
1015  "Dest concat file $dest exists after creation ($backendName)." );
1016  } else {
1017  $this->assertEquals( true, is_file( $dest ),
1018  "Dest concat file $dest exists after failed creation ($backendName)." );
1019  }
1020 
1021  $contents = file_get_contents( $dest );
1022  $this->assertNotEquals( false, $contents, "File at $dest exists ($backendName)." );
1023 
1024  if ( $okStatus ) {
1025  $this->assertEquals( $expContent, $contents,
1026  "Concat file at $dest has correct contents ($backendName)." );
1027  } else {
1028  $this->assertNotEquals( $expContent, $contents,
1029  "Concat file at $dest has correct contents ($backendName)." );
1030  }
1031  }
1032 
1033  public static function provider_testConcatenate() {
1034  $cases = [];
1035 
1036  $srcs = [
1037  self::baseStorePath() . '/unittest-cont1/e/file1.txt',
1038  self::baseStorePath() . '/unittest-cont1/e/file2.txt',
1039  self::baseStorePath() . '/unittest-cont1/e/file3.txt',
1040  self::baseStorePath() . '/unittest-cont1/e/file4.txt',
1041  self::baseStorePath() . '/unittest-cont1/e/file5.txt',
1042  self::baseStorePath() . '/unittest-cont1/e/file6.txt',
1043  self::baseStorePath() . '/unittest-cont1/e/file7.txt',
1044  self::baseStorePath() . '/unittest-cont1/e/file8.txt',
1045  self::baseStorePath() . '/unittest-cont1/e/file9.txt',
1046  self::baseStorePath() . '/unittest-cont1/e/file10.txt'
1047  ];
1048  $content = [
1049  'egfage',
1050  'ageageag',
1051  'rhokohlr',
1052  'shgmslkg',
1053  'kenga',
1054  'owagmal',
1055  'kgmae',
1056  'g eak;g',
1057  'lkaem;a',
1058  'legma'
1059  ];
1060  $params = [ 'srcs' => $srcs ];
1061 
1062  $cases[] = [
1063  $params, // operation
1064  $srcs, // sources
1065  $content, // content for each source
1066  false, // no dest already exists
1067  true, // succeeds
1068  ];
1069 
1070  $cases[] = [
1071  $params, // operation
1072  $srcs, // sources
1073  $content, // content for each source
1074  true, // dest already exists
1075  false, // succeeds
1076  ];
1077 
1078  return $cases;
1079  }
1080 
1084  public function testGetFileStat( $path, $content, $alreadyExists ) {
1085  $this->backend = $this->singleBackend;
1086  $this->tearDownFiles();
1087  $this->doTestGetFileStat( $path, $content, $alreadyExists );
1088  $this->tearDownFiles();
1089 
1090  $this->backend = $this->multiBackend;
1091  $this->tearDownFiles();
1092  $this->doTestGetFileStat( $path, $content, $alreadyExists );
1093  $this->tearDownFiles();
1094  }
1095 
1096  private function doTestGetFileStat( $path, $content, $alreadyExists ) {
1097  $backendName = $this->backendClass();
1098 
1099  if ( $alreadyExists ) {
1100  $this->prepare( [ 'dir' => dirname( $path ) ] );
1101  $status = $this->create( [ 'dst' => $path, 'content' => $content ] );
1102  $this->assertGoodStatus( $status,
1103  "Creation of file at $path succeeded ($backendName)." );
1104 
1105  $size = $this->backend->getFileSize( [ 'src' => $path ] );
1106  $time = $this->backend->getFileTimestamp( [ 'src' => $path ] );
1107  $stat = $this->backend->getFileStat( [ 'src' => $path ] );
1108 
1109  $this->assertEquals( strlen( $content ), $size,
1110  "Correct file size of '$path'" );
1111  $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10,
1112  "Correct file timestamp of '$path'" );
1113 
1114  $size = $stat['size'];
1115  $time = $stat['mtime'];
1116  $this->assertEquals( strlen( $content ), $size,
1117  "Correct file size of '$path'" );
1118  $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10,
1119  "Correct file timestamp of '$path'" );
1120 
1121  $this->backend->clearCache( [ $path ] );
1122 
1123  $size = $this->backend->getFileSize( [ 'src' => $path ] );
1124 
1125  $this->assertEquals( strlen( $content ), $size,
1126  "Correct file size of '$path'" );
1127 
1128  $this->backend->preloadCache( [ $path ] );
1129 
1130  $size = $this->backend->getFileSize( [ 'src' => $path ] );
1131 
1132  $this->assertEquals( strlen( $content ), $size,
1133  "Correct file size of '$path'" );
1134  } else {
1135  $size = $this->backend->getFileSize( [ 'src' => $path ] );
1136  $time = $this->backend->getFileTimestamp( [ 'src' => $path ] );
1137  $stat = $this->backend->getFileStat( [ 'src' => $path ] );
1138 
1139  $this->assertFalse( $size, "Correct file size of '$path'" );
1140  $this->assertFalse( $time, "Correct file timestamp of '$path'" );
1141  $this->assertFalse( $stat, "Correct file stat of '$path'" );
1142  }
1143  }
1144 
1145  public static function provider_testGetFileStat() {
1146  $cases = [];
1147 
1148  $base = self::baseStorePath();
1149  $cases[] = [ "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents", true ];
1150  $cases[] = [ "$base/unittest-cont1/e/b/some-other_file.txt", "", true ];
1151  $cases[] = [ "$base/unittest-cont1/e/b/some-diff_file.txt", null, false ];
1152 
1153  return $cases;
1154  }
1155 
1159  public function testStreamFile( $path, $content, $alreadyExists ) {
1160  $this->backend = $this->singleBackend;
1161  $this->tearDownFiles();
1162  $this->doTestStreamFile( $path, $content, $alreadyExists );
1163  $this->tearDownFiles();
1164 
1165  $this->backend = $this->multiBackend;
1166  $this->tearDownFiles();
1167  $this->doTestStreamFile( $path, $content, $alreadyExists );
1168  $this->tearDownFiles();
1169  }
1170 
1171  private function doTestStreamFile( $path, $content ) {
1172  $backendName = $this->backendClass();
1173 
1174  if ( $content !== null ) {
1175  $this->prepare( [ 'dir' => dirname( $path ) ] );
1176  $status = $this->create( [ 'dst' => $path, 'content' => $content ] );
1177  $this->assertGoodStatus( $status,
1178  "Creation of file at $path succeeded ($backendName)." );
1179 
1180  ob_start();
1181  $this->backend->streamFile( [ 'src' => $path, 'headless' => 1, 'allowOB' => 1 ] );
1182  $data = ob_get_contents();
1183  ob_end_clean();
1184 
1185  $this->assertEquals( $content, $data, "Correct content streamed from '$path'" );
1186  } else { // 404 case
1187  ob_start();
1188  $this->backend->streamFile( [ 'src' => $path, 'headless' => 1, 'allowOB' => 1 ] );
1189  $data = ob_get_contents();
1190  ob_end_clean();
1191 
1192  $this->assertRegExp( '#<h1>File not found</h1>#', $data,
1193  "Correct content streamed from '$path' ($backendName)" );
1194  }
1195  }
1196 
1197  public static function provider_testStreamFile() {
1198  $cases = [];
1199 
1200  $base = self::baseStorePath();
1201  $cases[] = [ "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents" ];
1202  $cases[] = [ "$base/unittest-cont1/e/b/some-other_file.txt", null ];
1203 
1204  return $cases;
1205  }
1206 
1207  public function testStreamFileRange() {
1208  $this->backend = $this->singleBackend;
1209  $this->tearDownFiles();
1210  $this->doTestStreamFileRange();
1211  $this->tearDownFiles();
1212 
1213  $this->backend = $this->multiBackend;
1214  $this->tearDownFiles();
1215  $this->doTestStreamFileRange();
1216  $this->tearDownFiles();
1217  }
1218 
1219  private function doTestStreamFileRange() {
1220  $backendName = $this->backendClass();
1221 
1222  $base = self::baseStorePath();
1223  $path = "$base/unittest-cont1/e/b/z/range_file.txt";
1224  $content = "0123456789ABCDEF";
1225 
1226  $this->prepare( [ 'dir' => dirname( $path ) ] );
1227  $status = $this->create( [ 'dst' => $path, 'content' => $content ] );
1228  $this->assertGoodStatus( $status,
1229  "Creation of file at $path succeeded ($backendName)." );
1230 
1231  static $ranges = [
1232  'bytes=0-0' => '0',
1233  'bytes=0-3' => '0123',
1234  'bytes=4-8' => '45678',
1235  'bytes=15-15' => 'F',
1236  'bytes=14-15' => 'EF',
1237  'bytes=-5' => 'BCDEF',
1238  'bytes=-1' => 'F',
1239  'bytes=10-16' => 'ABCDEF',
1240  'bytes=10-99' => 'ABCDEF',
1241  ];
1242 
1243  foreach ( $ranges as $range => $chunk ) {
1244  ob_start();
1245  $this->backend->streamFile( [ 'src' => $path, 'headless' => 1, 'allowOB' => 1,
1246  'options' => [ 'range' => $range ] ] );
1247  $data = ob_get_contents();
1248  ob_end_clean();
1249 
1250  $this->assertEquals( $chunk, $data, "Correct chunk streamed from '$path' for '$range'" );
1251  }
1252  }
1253 
1257  public function testGetFileContents( $source, $content ) {
1258  $this->backend = $this->singleBackend;
1259  $this->tearDownFiles();
1261  $this->tearDownFiles();
1262 
1263  $this->backend = $this->multiBackend;
1264  $this->tearDownFiles();
1266  $this->tearDownFiles();
1267  }
1268 
1269  private function doTestGetFileContents( $source, $content ) {
1270  $backendName = $this->backendClass();
1271 
1272  $srcs = (array)$source;
1273  $content = (array)$content;
1274  foreach ( $srcs as $i => $src ) {
1275  $this->prepare( [ 'dir' => dirname( $src ) ] );
1276  $status = $this->backend->doOperation(
1277  [ 'op' => 'create', 'content' => $content[$i], 'dst' => $src ] );
1278  $this->assertGoodStatus( $status,
1279  "Creation of file at $src succeeded ($backendName)." );
1280  }
1281 
1282  if ( is_array( $source ) ) {
1283  $contents = $this->backend->getFileContentsMulti( [ 'srcs' => $source ] );
1284  foreach ( $contents as $path => $data ) {
1285  $this->assertNotEquals( false, $data, "Contents of $path exists ($backendName)." );
1286  $this->assertEquals(
1287  current( $content ),
1288  $data,
1289  "Contents of $path is correct ($backendName)."
1290  );
1291  next( $content );
1292  }
1293  $this->assertEquals(
1294  $source,
1295  array_keys( $contents ),
1296  "Contents in right order ($backendName)."
1297  );
1298  $this->assertEquals(
1299  count( $source ),
1300  count( $contents ),
1301  "Contents array size correct ($backendName)."
1302  );
1303  } else {
1304  $data = $this->backend->getFileContents( [ 'src' => $source ] );
1305  $this->assertNotEquals( false, $data, "Contents of $source exists ($backendName)." );
1306  $this->assertEquals( $content[0], $data, "Contents of $source is correct ($backendName)." );
1307  }
1308  }
1309 
1310  public static function provider_testGetFileContents() {
1311  $cases = [];
1312 
1313  $base = self::baseStorePath();
1314  $cases[] = [ "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents" ];
1315  $cases[] = [ "$base/unittest-cont1/e/b/some-other_file.txt", "more file contents" ];
1316  $cases[] = [
1317  [ "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
1318  "$base/unittest-cont1/e/a/z.txt" ],
1319  [ "contents xx", "contents xy", "contents xz" ]
1320  ];
1321 
1322  return $cases;
1323  }
1324 
1328  public function testGetLocalCopy( $source, $content ) {
1329  $this->backend = $this->singleBackend;
1330  $this->tearDownFiles();
1331  $this->doTestGetLocalCopy( $source, $content );
1332  $this->tearDownFiles();
1333 
1334  $this->backend = $this->multiBackend;
1335  $this->tearDownFiles();
1336  $this->doTestGetLocalCopy( $source, $content );
1337  $this->tearDownFiles();
1338  }
1339 
1340  private function doTestGetLocalCopy( $source, $content ) {
1341  $backendName = $this->backendClass();
1342 
1343  $srcs = (array)$source;
1344  $content = (array)$content;
1345  foreach ( $srcs as $i => $src ) {
1346  $this->prepare( [ 'dir' => dirname( $src ) ] );
1347  $status = $this->backend->doOperation(
1348  [ 'op' => 'create', 'content' => $content[$i], 'dst' => $src ] );
1349  $this->assertGoodStatus( $status,
1350  "Creation of file at $src succeeded ($backendName)." );
1351  }
1352 
1353  if ( is_array( $source ) ) {
1354  $tmpFiles = $this->backend->getLocalCopyMulti( [ 'srcs' => $source ] );
1355  foreach ( $tmpFiles as $path => $tmpFile ) {
1356  $this->assertNotNull( $tmpFile,
1357  "Creation of local copy of $path succeeded ($backendName)." );
1358  $contents = file_get_contents( $tmpFile->getPath() );
1359  $this->assertNotEquals( false, $contents, "Local copy of $path exists ($backendName)." );
1360  $this->assertEquals(
1361  current( $content ),
1362  $contents,
1363  "Local copy of $path is correct ($backendName)."
1364  );
1365  next( $content );
1366  }
1367  $this->assertEquals(
1368  $source,
1369  array_keys( $tmpFiles ),
1370  "Local copies in right order ($backendName)."
1371  );
1372  $this->assertEquals(
1373  count( $source ),
1374  count( $tmpFiles ),
1375  "Local copies array size correct ($backendName)."
1376  );
1377  } else {
1378  $tmpFile = $this->backend->getLocalCopy( [ 'src' => $source ] );
1379  $this->assertNotNull( $tmpFile,
1380  "Creation of local copy of $source succeeded ($backendName)." );
1381  $contents = file_get_contents( $tmpFile->getPath() );
1382  $this->assertNotEquals( false, $contents, "Local copy of $source exists ($backendName)." );
1383  $this->assertEquals(
1384  $content[0],
1385  $contents,
1386  "Local copy of $source is correct ($backendName)."
1387  );
1388  }
1389 
1390  $obj = new stdClass();
1391  $tmpFile->bind( $obj );
1392  }
1393 
1394  public static function provider_testGetLocalCopy() {
1395  $cases = [];
1396 
1397  $base = self::baseStorePath();
1398  $cases[] = [ "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" ];
1399  $cases[] = [ "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" ];
1400  $cases[] = [ "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" ];
1401  $cases[] = [
1402  [ "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
1403  "$base/unittest-cont1/e/a/z.txt" ],
1404  [ "contents xx $", "contents xy 111", "contents xz" ]
1405  ];
1406 
1407  return $cases;
1408  }
1409 
1414  $this->backend = $this->singleBackend;
1415  $this->tearDownFiles();
1417  $this->tearDownFiles();
1418 
1419  $this->backend = $this->multiBackend;
1420  $this->tearDownFiles();
1422  $this->tearDownFiles();
1423  }
1424 
1426  $backendName = $this->backendClass();
1427 
1428  $srcs = (array)$source;
1429  $content = (array)$content;
1430  foreach ( $srcs as $i => $src ) {
1431  $this->prepare( [ 'dir' => dirname( $src ) ] );
1432  $status = $this->backend->doOperation(
1433  [ 'op' => 'create', 'content' => $content[$i], 'dst' => $src ] );
1434  $this->assertGoodStatus( $status,
1435  "Creation of file at $src succeeded ($backendName)." );
1436  }
1437 
1438  if ( is_array( $source ) ) {
1439  $tmpFiles = $this->backend->getLocalReferenceMulti( [ 'srcs' => $source ] );
1440  foreach ( $tmpFiles as $path => $tmpFile ) {
1441  $this->assertNotNull( $tmpFile,
1442  "Creation of local copy of $path succeeded ($backendName)." );
1443  $contents = file_get_contents( $tmpFile->getPath() );
1444  $this->assertNotEquals( false, $contents, "Local ref of $path exists ($backendName)." );
1445  $this->assertEquals(
1446  current( $content ),
1447  $contents,
1448  "Local ref of $path is correct ($backendName)."
1449  );
1450  next( $content );
1451  }
1452  $this->assertEquals(
1453  $source,
1454  array_keys( $tmpFiles ),
1455  "Local refs in right order ($backendName)."
1456  );
1457  $this->assertEquals(
1458  count( $source ),
1459  count( $tmpFiles ),
1460  "Local refs array size correct ($backendName)."
1461  );
1462  } else {
1463  $tmpFile = $this->backend->getLocalReference( [ 'src' => $source ] );
1464  $this->assertNotNull( $tmpFile,
1465  "Creation of local copy of $source succeeded ($backendName)." );
1466  $contents = file_get_contents( $tmpFile->getPath() );
1467  $this->assertNotEquals( false, $contents, "Local ref of $source exists ($backendName)." );
1468  $this->assertEquals( $content[0], $contents, "Local ref of $source is correct ($backendName)." );
1469  }
1470  }
1471 
1472  public static function provider_testGetLocalReference() {
1473  $cases = [];
1474 
1475  $base = self::baseStorePath();
1476  $cases[] = [ "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" ];
1477  $cases[] = [ "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" ];
1478  $cases[] = [ "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" ];
1479  $cases[] = [
1480  [ "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
1481  "$base/unittest-cont1/e/a/z.txt" ],
1482  [ "contents xx 1111", "contents xy %", "contents xz $" ]
1483  ];
1484 
1485  return $cases;
1486  }
1487 
1489  $this->backend = $this->singleBackend;
1490  $this->tearDownFiles();
1492  $this->tearDownFiles();
1493 
1494  $this->backend = $this->multiBackend;
1495  $this->tearDownFiles();
1497  $this->tearDownFiles();
1498  }
1499 
1501  $backendName = $this->backendClass();
1502 
1503  $base = self::baseStorePath();
1504 
1505  $tmpFile = $this->backend->getLocalCopy( [
1506  'src' => "$base/unittest-cont1/not-there" ] );
1507  $this->assertEquals( null, $tmpFile, "Local copy of not existing file is null ($backendName)." );
1508 
1509  $tmpFile = $this->backend->getLocalReference( [
1510  'src' => "$base/unittest-cont1/not-there" ] );
1511  $this->assertEquals( null, $tmpFile, "Local ref of not existing file is null ($backendName)." );
1512  }
1513 
1517  public function testGetFileHttpUrl( $source, $content ) {
1518  $this->backend = $this->singleBackend;
1519  $this->tearDownFiles();
1521  $this->tearDownFiles();
1522 
1523  $this->backend = $this->multiBackend;
1524  $this->tearDownFiles();
1526  $this->tearDownFiles();
1527  }
1528 
1529  private function doTestGetFileHttpUrl( $source, $content ) {
1530  $backendName = $this->backendClass();
1531 
1532  $this->prepare( [ 'dir' => dirname( $source ) ] );
1533  $status = $this->backend->doOperation(
1534  [ 'op' => 'create', 'content' => $content, 'dst' => $source ] );
1535  $this->assertGoodStatus( $status,
1536  "Creation of file at $source succeeded ($backendName)." );
1537 
1538  $url = $this->backend->getFileHttpUrl( [ 'src' => $source ] );
1539 
1540  if ( $url !== null ) { // supported
1541  $data = Http::request( "GET", $url, [], __METHOD__ );
1542  $this->assertEquals( $content, $data,
1543  "HTTP GET of URL has right contents ($backendName)." );
1544  }
1545  }
1546 
1547  public static function provider_testGetFileHttpUrl() {
1548  $cases = [];
1549 
1550  $base = self::baseStorePath();
1551  $cases[] = [ "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" ];
1552  $cases[] = [ "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" ];
1553  $cases[] = [ "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" ];
1554 
1555  return $cases;
1556  }
1557 
1561  public function testPrepareAndClean( $path, $isOK ) {
1562  $this->backend = $this->singleBackend;
1563  $this->doTestPrepareAndClean( $path, $isOK );
1564  $this->tearDownFiles();
1565 
1566  $this->backend = $this->multiBackend;
1567  $this->doTestPrepareAndClean( $path, $isOK );
1568  $this->tearDownFiles();
1569  }
1570 
1571  public static function provider_testPrepareAndClean() {
1572  $base = self::baseStorePath();
1573 
1574  return [
1575  [ "$base/unittest-cont1/e/a/z/some_file1.txt", true ],
1576  [ "$base/unittest-cont2/a/z/some_file2.txt", true ],
1577  # Specific to FS backend with no basePath field set
1578  # [ "$base/unittest-cont3/a/z/some_file3.txt", false ],
1579  ];
1580  }
1581 
1582  private function doTestPrepareAndClean( $path, $isOK ) {
1583  $backendName = $this->backendClass();
1584 
1585  $status = $this->prepare( [ 'dir' => dirname( $path ) ] );
1586  if ( $isOK ) {
1587  $this->assertGoodStatus( $status,
1588  "Preparing dir $path succeeded without warnings ($backendName)." );
1589  $this->assertEquals( true, $status->isOK(),
1590  "Preparing dir $path succeeded ($backendName)." );
1591  } else {
1592  $this->assertEquals( false, $status->isOK(),
1593  "Preparing dir $path failed ($backendName)." );
1594  }
1595 
1596  $status = $this->backend->secure( [ 'dir' => dirname( $path ) ] );
1597  if ( $isOK ) {
1598  $this->assertGoodStatus( $status,
1599  "Securing dir $path succeeded without warnings ($backendName)." );
1600  $this->assertEquals( true, $status->isOK(),
1601  "Securing dir $path succeeded ($backendName)." );
1602  } else {
1603  $this->assertEquals( false, $status->isOK(),
1604  "Securing dir $path failed ($backendName)." );
1605  }
1606 
1607  $status = $this->backend->publish( [ 'dir' => dirname( $path ) ] );
1608  if ( $isOK ) {
1609  $this->assertGoodStatus( $status,
1610  "Publishing dir $path succeeded without warnings ($backendName)." );
1611  $this->assertEquals( true, $status->isOK(),
1612  "Publishing dir $path succeeded ($backendName)." );
1613  } else {
1614  $this->assertEquals( false, $status->isOK(),
1615  "Publishing dir $path failed ($backendName)." );
1616  }
1617 
1618  $status = $this->backend->clean( [ 'dir' => dirname( $path ) ] );
1619  if ( $isOK ) {
1620  $this->assertGoodStatus( $status,
1621  "Cleaning dir $path succeeded without warnings ($backendName)." );
1622  $this->assertEquals( true, $status->isOK(),
1623  "Cleaning dir $path succeeded ($backendName)." );
1624  } else {
1625  $this->assertEquals( false, $status->isOK(),
1626  "Cleaning dir $path failed ($backendName)." );
1627  }
1628  }
1629 
1630  public function testRecursiveClean() {
1631  $this->backend = $this->singleBackend;
1632  $this->doTestRecursiveClean();
1633  $this->tearDownFiles();
1634 
1635  $this->backend = $this->multiBackend;
1636  $this->doTestRecursiveClean();
1637  $this->tearDownFiles();
1638  }
1639 
1640  private function doTestRecursiveClean() {
1641  $backendName = $this->backendClass();
1642 
1643  $base = self::baseStorePath();
1644  $dirs = [
1645  "$base/unittest-cont1",
1646  "$base/unittest-cont1/e",
1647  "$base/unittest-cont1/e/a",
1648  "$base/unittest-cont1/e/a/b",
1649  "$base/unittest-cont1/e/a/b/c",
1650  "$base/unittest-cont1/e/a/b/c/d0",
1651  "$base/unittest-cont1/e/a/b/c/d1",
1652  "$base/unittest-cont1/e/a/b/c/d2",
1653  "$base/unittest-cont1/e/a/b/c/d0/1",
1654  "$base/unittest-cont1/e/a/b/c/d0/2",
1655  "$base/unittest-cont1/e/a/b/c/d1/3",
1656  "$base/unittest-cont1/e/a/b/c/d1/4",
1657  "$base/unittest-cont1/e/a/b/c/d2/5",
1658  "$base/unittest-cont1/e/a/b/c/d2/6"
1659  ];
1660  foreach ( $dirs as $dir ) {
1661  $status = $this->prepare( [ 'dir' => $dir ] );
1662  $this->assertGoodStatus( $status,
1663  "Preparing dir $dir succeeded without warnings ($backendName)." );
1664  }
1665 
1666  if ( $this->backend instanceof FSFileBackend ) {
1667  foreach ( $dirs as $dir ) {
1668  $this->assertEquals( true, $this->backend->directoryExists( [ 'dir' => $dir ] ),
1669  "Dir $dir exists ($backendName)." );
1670  }
1671  }
1672 
1673  $status = $this->backend->clean(
1674  [ 'dir' => "$base/unittest-cont1", 'recursive' => 1 ] );
1675  $this->assertGoodStatus( $status,
1676  "Recursive cleaning of dir $dir succeeded without warnings ($backendName)." );
1677 
1678  foreach ( $dirs as $dir ) {
1679  $this->assertEquals( false, $this->backend->directoryExists( [ 'dir' => $dir ] ),
1680  "Dir $dir no longer exists ($backendName)." );
1681  }
1682  }
1683 
1684  public function testDoOperations() {
1685  $this->backend = $this->singleBackend;
1686  $this->tearDownFiles();
1687  $this->doTestDoOperations();
1688  $this->tearDownFiles();
1689 
1690  $this->backend = $this->multiBackend;
1691  $this->tearDownFiles();
1692  $this->doTestDoOperations();
1693  $this->tearDownFiles();
1694  }
1695 
1696  private function doTestDoOperations() {
1697  $base = self::baseStorePath();
1698 
1699  $fileA = "$base/unittest-cont1/e/a/b/fileA.txt";
1700  $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
1701  $fileB = "$base/unittest-cont1/e/a/b/fileB.txt";
1702  $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
1703  $fileC = "$base/unittest-cont1/e/a/b/fileC.txt";
1704  $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
1705  $fileD = "$base/unittest-cont1/e/a/b/fileD.txt";
1706 
1707  $this->prepare( [ 'dir' => dirname( $fileA ) ] );
1708  $this->create( [ 'dst' => $fileA, 'content' => $fileAContents ] );
1709  $this->prepare( [ 'dir' => dirname( $fileB ) ] );
1710  $this->create( [ 'dst' => $fileB, 'content' => $fileBContents ] );
1711  $this->prepare( [ 'dir' => dirname( $fileC ) ] );
1712  $this->create( [ 'dst' => $fileC, 'content' => $fileCContents ] );
1713  $this->prepare( [ 'dir' => dirname( $fileD ) ] );
1714 
1715  $status = $this->backend->doOperations( [
1716  [ 'op' => 'describe', 'src' => $fileA,
1717  'headers' => [ 'X-Content-Length' => '91.3' ], 'disposition' => 'inline' ],
1718  [ 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ],
1719  // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
1720  [ 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ],
1721  // Now: A:<A>, B:<B>, C:<A>, D:<empty>
1722  [ 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ],
1723  // Now: A:<A>, B:<B>, C:<empty>, D:<A>
1724  [ 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ],
1725  // Now: A:<A>, B:<empty>, C:<B>, D:<A>
1726  [ 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ],
1727  // Now: A:<A>, B:<empty>, C:<B>, D:<empty>
1728  [ 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ],
1729  // Now: A:<B>, B:<empty>, C:<empty>, D:<empty>
1730  [ 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ],
1731  // Now: A:<B>, B:<empty>, C:<B>, D:<empty>
1732  [ 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ],
1733  // Now: A:<empty>, B:<empty>, C:<B>, D:<empty>
1734  [ 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ],
1735  // Does nothing
1736  [ 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ],
1737  // Does nothing
1738  [ 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ],
1739  // Does nothing
1740  [ 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ],
1741  // Does nothing
1742  [ 'op' => 'null' ],
1743  // Does nothing
1744  ] );
1745 
1746  $this->assertGoodStatus( $status, "Operation batch succeeded" );
1747  $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
1748  $this->assertEquals( 14, count( $status->success ),
1749  "Operation batch has correct success array" );
1750 
1751  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $fileA ] ),
1752  "File does not exist at $fileA" );
1753  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $fileB ] ),
1754  "File does not exist at $fileB" );
1755  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $fileD ] ),
1756  "File does not exist at $fileD" );
1757 
1758  $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $fileC ] ),
1759  "File exists at $fileC" );
1760  $this->assertEquals( $fileBContents,
1761  $this->backend->getFileContents( [ 'src' => $fileC ] ),
1762  "Correct file contents of $fileC" );
1763  $this->assertEquals( strlen( $fileBContents ),
1764  $this->backend->getFileSize( [ 'src' => $fileC ] ),
1765  "Correct file size of $fileC" );
1766  $this->assertEquals( Wikimedia\base_convert( sha1( $fileBContents ), 16, 36, 31 ),
1767  $this->backend->getFileSha1Base36( [ 'src' => $fileC ] ),
1768  "Correct file SHA-1 of $fileC" );
1769  }
1770 
1771  public function testDoOperationsPipeline() {
1772  $this->backend = $this->singleBackend;
1773  $this->tearDownFiles();
1774  $this->doTestDoOperationsPipeline();
1775  $this->tearDownFiles();
1776 
1777  $this->backend = $this->multiBackend;
1778  $this->tearDownFiles();
1779  $this->doTestDoOperationsPipeline();
1780  $this->tearDownFiles();
1781  }
1782 
1783  // concurrency orientated
1784  private function doTestDoOperationsPipeline() {
1785  $base = self::baseStorePath();
1786 
1787  $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
1788  $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
1789  $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
1790 
1791  $tmpNameA = TempFSFile::factory( "unittests_", 'txt', wfTempDir() )->getPath();
1792  $tmpNameB = TempFSFile::factory( "unittests_", 'txt', wfTempDir() )->getPath();
1793  $tmpNameC = TempFSFile::factory( "unittests_", 'txt', wfTempDir() )->getPath();
1794  $this->addTmpFiles( [ $tmpNameA, $tmpNameB, $tmpNameC ] );
1795  file_put_contents( $tmpNameA, $fileAContents );
1796  file_put_contents( $tmpNameB, $fileBContents );
1797  file_put_contents( $tmpNameC, $fileCContents );
1798 
1799  $fileA = "$base/unittest-cont1/e/a/b/fileA.txt";
1800  $fileB = "$base/unittest-cont1/e/a/b/fileB.txt";
1801  $fileC = "$base/unittest-cont1/e/a/b/fileC.txt";
1802  $fileD = "$base/unittest-cont1/e/a/b/fileD.txt";
1803 
1804  $this->prepare( [ 'dir' => dirname( $fileA ) ] );
1805  $this->create( [ 'dst' => $fileA, 'content' => $fileAContents ] );
1806  $this->prepare( [ 'dir' => dirname( $fileB ) ] );
1807  $this->prepare( [ 'dir' => dirname( $fileC ) ] );
1808  $this->prepare( [ 'dir' => dirname( $fileD ) ] );
1809 
1810  $status = $this->backend->doOperations( [
1811  [ 'op' => 'store', 'src' => $tmpNameA, 'dst' => $fileA, 'overwriteSame' => 1 ],
1812  [ 'op' => 'store', 'src' => $tmpNameB, 'dst' => $fileB, 'overwrite' => 1 ],
1813  [ 'op' => 'store', 'src' => $tmpNameC, 'dst' => $fileC, 'overwrite' => 1 ],
1814  [ 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ],
1815  // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
1816  [ 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ],
1817  // Now: A:<A>, B:<B>, C:<A>, D:<empty>
1818  [ 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ],
1819  // Now: A:<A>, B:<B>, C:<empty>, D:<A>
1820  [ 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ],
1821  // Now: A:<A>, B:<empty>, C:<B>, D:<A>
1822  [ 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ],
1823  // Now: A:<A>, B:<empty>, C:<B>, D:<empty>
1824  [ 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ],
1825  // Now: A:<B>, B:<empty>, C:<empty>, D:<empty>
1826  [ 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ],
1827  // Now: A:<B>, B:<empty>, C:<B>, D:<empty>
1828  [ 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ],
1829  // Now: A:<empty>, B:<empty>, C:<B>, D:<empty>
1830  [ 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ],
1831  // Does nothing
1832  [ 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ],
1833  // Does nothing
1834  [ 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ],
1835  // Does nothing
1836  [ 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ],
1837  // Does nothing
1838  [ 'op' => 'null' ],
1839  // Does nothing
1840  ] );
1841 
1842  $this->assertGoodStatus( $status, "Operation batch succeeded" );
1843  $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
1844  $this->assertEquals( 16, count( $status->success ),
1845  "Operation batch has correct success array" );
1846 
1847  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $fileA ] ),
1848  "File does not exist at $fileA" );
1849  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $fileB ] ),
1850  "File does not exist at $fileB" );
1851  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $fileD ] ),
1852  "File does not exist at $fileD" );
1853 
1854  $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $fileC ] ),
1855  "File exists at $fileC" );
1856  $this->assertEquals( $fileBContents,
1857  $this->backend->getFileContents( [ 'src' => $fileC ] ),
1858  "Correct file contents of $fileC" );
1859  $this->assertEquals( strlen( $fileBContents ),
1860  $this->backend->getFileSize( [ 'src' => $fileC ] ),
1861  "Correct file size of $fileC" );
1862  $this->assertEquals( Wikimedia\base_convert( sha1( $fileBContents ), 16, 36, 31 ),
1863  $this->backend->getFileSha1Base36( [ 'src' => $fileC ] ),
1864  "Correct file SHA-1 of $fileC" );
1865  }
1866 
1867  public function testDoOperationsFailing() {
1868  $this->backend = $this->singleBackend;
1869  $this->tearDownFiles();
1870  $this->doTestDoOperationsFailing();
1871  $this->tearDownFiles();
1872 
1873  $this->backend = $this->multiBackend;
1874  $this->tearDownFiles();
1875  $this->doTestDoOperationsFailing();
1876  $this->tearDownFiles();
1877  }
1878 
1879  private function doTestDoOperationsFailing() {
1880  $base = self::baseStorePath();
1881 
1882  $fileA = "$base/unittest-cont2/a/b/fileA.txt";
1883  $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
1884  $fileB = "$base/unittest-cont2/a/b/fileB.txt";
1885  $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
1886  $fileC = "$base/unittest-cont2/a/b/fileC.txt";
1887  $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
1888  $fileD = "$base/unittest-cont2/a/b/fileD.txt";
1889 
1890  $this->prepare( [ 'dir' => dirname( $fileA ) ] );
1891  $this->create( [ 'dst' => $fileA, 'content' => $fileAContents ] );
1892  $this->prepare( [ 'dir' => dirname( $fileB ) ] );
1893  $this->create( [ 'dst' => $fileB, 'content' => $fileBContents ] );
1894  $this->prepare( [ 'dir' => dirname( $fileC ) ] );
1895  $this->create( [ 'dst' => $fileC, 'content' => $fileCContents ] );
1896 
1897  $status = $this->backend->doOperations( [
1898  [ 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ],
1899  // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
1900  [ 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ],
1901  // Now: A:<A>, B:<B>, C:<A>, D:<empty>
1902  [ 'op' => 'copy', 'src' => $fileB, 'dst' => $fileD, 'overwrite' => 1 ],
1903  // Now: A:<A>, B:<B>, C:<A>, D:<B>
1904  [ 'op' => 'move', 'src' => $fileC, 'dst' => $fileD ],
1905  // Now: A:<A>, B:<B>, C:<A>, D:<empty> (failed)
1906  [ 'op' => 'move', 'src' => $fileB, 'dst' => $fileC, 'overwriteSame' => 1 ],
1907  // Now: A:<A>, B:<B>, C:<A>, D:<empty> (failed)
1908  [ 'op' => 'move', 'src' => $fileB, 'dst' => $fileA, 'overwrite' => 1 ],
1909  // Now: A:<B>, B:<empty>, C:<A>, D:<empty>
1910  [ 'op' => 'delete', 'src' => $fileD ],
1911  // Now: A:<B>, B:<empty>, C:<A>, D:<empty>
1912  [ 'op' => 'null' ],
1913  // Does nothing
1914  ], [ 'force' => 1 ] );
1915 
1916  $this->assertNotEquals( [], $status->getErrors(), "Operation had warnings" );
1917  $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
1918  $this->assertEquals( 8, count( $status->success ),
1919  "Operation batch has correct success array" );
1920 
1921  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $fileB ] ),
1922  "File does not exist at $fileB" );
1923  $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $fileD ] ),
1924  "File does not exist at $fileD" );
1925 
1926  $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $fileA ] ),
1927  "File does not exist at $fileA" );
1928  $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $fileC ] ),
1929  "File exists at $fileC" );
1930  $this->assertEquals( $fileBContents,
1931  $this->backend->getFileContents( [ 'src' => $fileA ] ),
1932  "Correct file contents of $fileA" );
1933  $this->assertEquals( strlen( $fileBContents ),
1934  $this->backend->getFileSize( [ 'src' => $fileA ] ),
1935  "Correct file size of $fileA" );
1936  $this->assertEquals( Wikimedia\base_convert( sha1( $fileBContents ), 16, 36, 31 ),
1937  $this->backend->getFileSha1Base36( [ 'src' => $fileA ] ),
1938  "Correct file SHA-1 of $fileA" );
1939  }
1940 
1941  public function testGetFileList() {
1942  $this->backend = $this->singleBackend;
1943  $this->tearDownFiles();
1944  $this->doTestGetFileList();
1945  $this->tearDownFiles();
1946 
1947  $this->backend = $this->multiBackend;
1948  $this->tearDownFiles();
1949  $this->doTestGetFileList();
1950  $this->tearDownFiles();
1951  }
1952 
1953  private function doTestGetFileList() {
1954  $backendName = $this->backendClass();
1955  $base = self::baseStorePath();
1956 
1957  // Should have no errors
1958  $iter = $this->backend->getFileList( [ 'dir' => "$base/unittest-cont-notexists" ] );
1959 
1960  $files = [
1961  "$base/unittest-cont1/e/test1.txt",
1962  "$base/unittest-cont1/e/test2.txt",
1963  "$base/unittest-cont1/e/test3.txt",
1964  "$base/unittest-cont1/e/subdir1/test1.txt",
1965  "$base/unittest-cont1/e/subdir1/test2.txt",
1966  "$base/unittest-cont1/e/subdir2/test3.txt",
1967  "$base/unittest-cont1/e/subdir2/test4.txt",
1968  "$base/unittest-cont1/e/subdir2/subdir/test1.txt",
1969  "$base/unittest-cont1/e/subdir2/subdir/test2.txt",
1970  "$base/unittest-cont1/e/subdir2/subdir/test3.txt",
1971  "$base/unittest-cont1/e/subdir2/subdir/test4.txt",
1972  "$base/unittest-cont1/e/subdir2/subdir/test5.txt",
1973  "$base/unittest-cont1/e/subdir2/subdir/sub/test0.txt",
1974  "$base/unittest-cont1/e/subdir2/subdir/sub/120-px-file.txt",
1975  ];
1976 
1977  // Add the files
1978  $ops = [];
1979  foreach ( $files as $file ) {
1980  $this->prepare( [ 'dir' => dirname( $file ) ] );
1981  $ops[] = [ 'op' => 'create', 'content' => 'xxy', 'dst' => $file ];
1982  }
1983  $status = $this->backend->doQuickOperations( $ops );
1984  $this->assertGoodStatus( $status,
1985  "Creation of files succeeded ($backendName)." );
1986  $this->assertEquals( true, $status->isOK(),
1987  "Creation of files succeeded with OK status ($backendName)." );
1988 
1989  // Expected listing at root
1990  $expected = [
1991  "e/test1.txt",
1992  "e/test2.txt",
1993  "e/test3.txt",
1994  "e/subdir1/test1.txt",
1995  "e/subdir1/test2.txt",
1996  "e/subdir2/test3.txt",
1997  "e/subdir2/test4.txt",
1998  "e/subdir2/subdir/test1.txt",
1999  "e/subdir2/subdir/test2.txt",
2000  "e/subdir2/subdir/test3.txt",
2001  "e/subdir2/subdir/test4.txt",
2002  "e/subdir2/subdir/test5.txt",
2003  "e/subdir2/subdir/sub/test0.txt",
2004  "e/subdir2/subdir/sub/120-px-file.txt",
2005  ];
2006  sort( $expected );
2007 
2008  // Actual listing (no trailing slash) at root
2009  $iter = $this->backend->getFileList( [ 'dir' => "$base/unittest-cont1" ] );
2010  $list = $this->listToArray( $iter );
2011  sort( $list );
2012  $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
2013 
2014  // Actual listing (no trailing slash) at root with advise
2015  $iter = $this->backend->getFileList( [
2016  'dir' => "$base/unittest-cont1",
2017  'adviseStat' => 1
2018  ] );
2019  $list = $this->listToArray( $iter );
2020  sort( $list );
2021  $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
2022 
2023  // Actual listing (with trailing slash) at root
2024  $list = [];
2025  $iter = $this->backend->getFileList( [ 'dir' => "$base/unittest-cont1/" ] );
2026  foreach ( $iter as $file ) {
2027  $list[] = $file;
2028  }
2029  sort( $list );
2030  $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
2031 
2032  // Expected listing at subdir
2033  $expected = [
2034  "test1.txt",
2035  "test2.txt",
2036  "test3.txt",
2037  "test4.txt",
2038  "test5.txt",
2039  "sub/test0.txt",
2040  "sub/120-px-file.txt",
2041  ];
2042  sort( $expected );
2043 
2044  // Actual listing (no trailing slash) at subdir
2045  $iter = $this->backend->getFileList( [ 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ] );
2046  $list = $this->listToArray( $iter );
2047  sort( $list );
2048  $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
2049 
2050  // Actual listing (no trailing slash) at subdir with advise
2051  $iter = $this->backend->getFileList( [
2052  'dir' => "$base/unittest-cont1/e/subdir2/subdir",
2053  'adviseStat' => 1
2054  ] );
2055  $list = $this->listToArray( $iter );
2056  sort( $list );
2057  $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
2058 
2059  // Actual listing (with trailing slash) at subdir
2060  $list = [];
2061  $iter = $this->backend->getFileList( [ 'dir' => "$base/unittest-cont1/e/subdir2/subdir/" ] );
2062  foreach ( $iter as $file ) {
2063  $list[] = $file;
2064  }
2065  sort( $list );
2066  $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
2067 
2068  // Actual listing (using iterator second time)
2069  $list = $this->listToArray( $iter );
2070  sort( $list );
2071  $this->assertEquals( $expected, $list, "Correct file listing ($backendName), second iteration." );
2072 
2073  // Actual listing (top files only) at root
2074  $iter = $this->backend->getTopFileList( [ 'dir' => "$base/unittest-cont1" ] );
2075  $list = $this->listToArray( $iter );
2076  sort( $list );
2077  $this->assertEquals( [], $list, "Correct top file listing ($backendName)." );
2078 
2079  // Expected listing (top files only) at subdir
2080  $expected = [
2081  "test1.txt",
2082  "test2.txt",
2083  "test3.txt",
2084  "test4.txt",
2085  "test5.txt"
2086  ];
2087  sort( $expected );
2088 
2089  // Actual listing (top files only) at subdir
2090  $iter = $this->backend->getTopFileList(
2091  [ 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ]
2092  );
2093  $list = $this->listToArray( $iter );
2094  sort( $list );
2095  $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." );
2096 
2097  // Actual listing (top files only) at subdir with advise
2098  $iter = $this->backend->getTopFileList( [
2099  'dir' => "$base/unittest-cont1/e/subdir2/subdir",
2100  'adviseStat' => 1
2101  ] );
2102  $list = $this->listToArray( $iter );
2103  sort( $list );
2104  $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." );
2105 
2106  foreach ( $files as $file ) { // clean up
2107  $this->backend->doOperation( [ 'op' => 'delete', 'src' => $file ] );
2108  }
2109 
2110  $iter = $this->backend->getFileList( [ 'dir' => "$base/unittest-cont1/not/exists" ] );
2111  foreach ( $iter as $iter ) {
2112  // no errors
2113  }
2114  }
2115 
2116  public function testGetDirectoryList() {
2117  $this->backend = $this->singleBackend;
2118  $this->tearDownFiles();
2119  $this->doTestGetDirectoryList();
2120  $this->tearDownFiles();
2121 
2122  $this->backend = $this->multiBackend;
2123  $this->tearDownFiles();
2124  $this->doTestGetDirectoryList();
2125  $this->tearDownFiles();
2126  }
2127 
2128  private function doTestGetDirectoryList() {
2129  $backendName = $this->backendClass();
2130 
2131  $base = self::baseStorePath();
2132  $files = [
2133  "$base/unittest-cont1/e/test1.txt",
2134  "$base/unittest-cont1/e/test2.txt",
2135  "$base/unittest-cont1/e/test3.txt",
2136  "$base/unittest-cont1/e/subdir1/test1.txt",
2137  "$base/unittest-cont1/e/subdir1/test2.txt",
2138  "$base/unittest-cont1/e/subdir2/test3.txt",
2139  "$base/unittest-cont1/e/subdir2/test4.txt",
2140  "$base/unittest-cont1/e/subdir2/subdir/test1.txt",
2141  "$base/unittest-cont1/e/subdir3/subdir/test2.txt",
2142  "$base/unittest-cont1/e/subdir4/subdir/test3.txt",
2143  "$base/unittest-cont1/e/subdir4/subdir/test4.txt",
2144  "$base/unittest-cont1/e/subdir4/subdir/test5.txt",
2145  "$base/unittest-cont1/e/subdir4/subdir/sub/test0.txt",
2146  "$base/unittest-cont1/e/subdir4/subdir/sub/120-px-file.txt",
2147  ];
2148 
2149  // Add the files
2150  $ops = [];
2151  foreach ( $files as $file ) {
2152  $this->prepare( [ 'dir' => dirname( $file ) ] );
2153  $ops[] = [ 'op' => 'create', 'content' => 'xxy', 'dst' => $file ];
2154  }
2155  $status = $this->backend->doQuickOperations( $ops );
2156  $this->assertGoodStatus( $status,
2157  "Creation of files succeeded ($backendName)." );
2158  $this->assertEquals( true, $status->isOK(),
2159  "Creation of files succeeded with OK status ($backendName)." );
2160 
2161  $this->assertEquals( true,
2162  $this->backend->directoryExists( [ 'dir' => "$base/unittest-cont1/e/subdir1" ] ),
2163  "Directory exists in ($backendName)." );
2164  $this->assertEquals( true,
2165  $this->backend->directoryExists( [ 'dir' => "$base/unittest-cont1/e/subdir2/subdir" ] ),
2166  "Directory exists in ($backendName)." );
2167  $this->assertEquals( false,
2168  $this->backend->directoryExists( [ 'dir' => "$base/unittest-cont1/e/subdir2/test1.txt" ] ),
2169  "Directory does not exists in ($backendName)." );
2170 
2171  // Expected listing
2172  $expected = [
2173  "e",
2174  ];
2175  sort( $expected );
2176 
2177  // Actual listing (no trailing slash)
2178  $list = [];
2179  $iter = $this->backend->getTopDirectoryList( [ 'dir' => "$base/unittest-cont1" ] );
2180  foreach ( $iter as $file ) {
2181  $list[] = $file;
2182  }
2183  sort( $list );
2184 
2185  $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
2186 
2187  // Expected listing
2188  $expected = [
2189  "subdir1",
2190  "subdir2",
2191  "subdir3",
2192  "subdir4",
2193  ];
2194  sort( $expected );
2195 
2196  // Actual listing (no trailing slash)
2197  $list = [];
2198  $iter = $this->backend->getTopDirectoryList( [ 'dir' => "$base/unittest-cont1/e" ] );
2199  foreach ( $iter as $file ) {
2200  $list[] = $file;
2201  }
2202  sort( $list );
2203 
2204  $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
2205 
2206  // Actual listing (with trailing slash)
2207  $list = [];
2208  $iter = $this->backend->getTopDirectoryList( [ 'dir' => "$base/unittest-cont1/e/" ] );
2209  foreach ( $iter as $file ) {
2210  $list[] = $file;
2211  }
2212  sort( $list );
2213 
2214  $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
2215 
2216  // Expected listing
2217  $expected = [
2218  "subdir",
2219  ];
2220  sort( $expected );
2221 
2222  // Actual listing (no trailing slash)
2223  $list = [];
2224  $iter = $this->backend->getTopDirectoryList( [ 'dir' => "$base/unittest-cont1/e/subdir2" ] );
2225  foreach ( $iter as $file ) {
2226  $list[] = $file;
2227  }
2228  sort( $list );
2229 
2230  $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
2231 
2232  // Actual listing (with trailing slash)
2233  $list = [];
2234  $iter = $this->backend->getTopDirectoryList(
2235  [ 'dir' => "$base/unittest-cont1/e/subdir2/" ]
2236  );
2237 
2238  foreach ( $iter as $file ) {
2239  $list[] = $file;
2240  }
2241  sort( $list );
2242 
2243  $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
2244 
2245  // Actual listing (using iterator second time)
2246  $list = [];
2247  foreach ( $iter as $file ) {
2248  $list[] = $file;
2249  }
2250  sort( $list );
2251 
2252  $this->assertEquals(
2253  $expected,
2254  $list,
2255  "Correct top dir listing ($backendName), second iteration."
2256  );
2257 
2258  // Expected listing (recursive)
2259  $expected = [
2260  "e",
2261  "e/subdir1",
2262  "e/subdir2",
2263  "e/subdir3",
2264  "e/subdir4",
2265  "e/subdir2/subdir",
2266  "e/subdir3/subdir",
2267  "e/subdir4/subdir",
2268  "e/subdir4/subdir/sub",
2269  ];
2270  sort( $expected );
2271 
2272  // Actual listing (recursive)
2273  $list = [];
2274  $iter = $this->backend->getDirectoryList( [ 'dir' => "$base/unittest-cont1/" ] );
2275  foreach ( $iter as $file ) {
2276  $list[] = $file;
2277  }
2278  sort( $list );
2279 
2280  $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
2281 
2282  // Expected listing (recursive)
2283  $expected = [
2284  "subdir",
2285  "subdir/sub",
2286  ];
2287  sort( $expected );
2288 
2289  // Actual listing (recursive)
2290  $list = [];
2291  $iter = $this->backend->getDirectoryList( [ 'dir' => "$base/unittest-cont1/e/subdir4" ] );
2292  foreach ( $iter as $file ) {
2293  $list[] = $file;
2294  }
2295  sort( $list );
2296 
2297  $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
2298 
2299  // Actual listing (recursive, second time)
2300  $list = [];
2301  foreach ( $iter as $file ) {
2302  $list[] = $file;
2303  }
2304  sort( $list );
2305 
2306  $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
2307 
2308  $iter = $this->backend->getDirectoryList( [ 'dir' => "$base/unittest-cont1/e/subdir1" ] );
2309  $items = $this->listToArray( $iter );
2310  $this->assertEquals( [], $items, "Directory listing is empty." );
2311 
2312  foreach ( $files as $file ) { // clean up
2313  $this->backend->doOperation( [ 'op' => 'delete', 'src' => $file ] );
2314  }
2315 
2316  $iter = $this->backend->getDirectoryList( [ 'dir' => "$base/unittest-cont1/not/exists" ] );
2317  foreach ( $iter as $file ) {
2318  // no errors
2319  }
2320 
2321  $items = $this->listToArray( $iter );
2322  $this->assertEquals( [], $items, "Directory listing is empty." );
2323 
2324  $iter = $this->backend->getDirectoryList( [ 'dir' => "$base/unittest-cont1/e/not/exists" ] );
2325  $items = $this->listToArray( $iter );
2326  $this->assertEquals( [], $items, "Directory listing is empty." );
2327  }
2328 
2329  public function testLockCalls() {
2330  $this->backend = $this->singleBackend;
2331  $this->doTestLockCalls();
2332  }
2333 
2334  private function doTestLockCalls() {
2335  $backendName = $this->backendClass();
2336  $base = $this->backend->getContainerStoragePath( 'test' );
2337 
2338  $paths = [
2339  "$base/test1.txt",
2340  "$base/test2.txt",
2341  "$base/test3.txt",
2342  "$base/subdir1",
2343  "$base/subdir1", // duplicate
2344  "$base/subdir1/test1.txt",
2345  "$base/subdir1/test2.txt",
2346  "$base/subdir2",
2347  "$base/subdir2", // duplicate
2348  "$base/subdir2/test3.txt",
2349  "$base/subdir2/test4.txt",
2350  "$base/subdir2/subdir",
2351  "$base/subdir2/subdir/test1.txt",
2352  "$base/subdir2/subdir/test2.txt",
2353  "$base/subdir2/subdir/test3.txt",
2354  "$base/subdir2/subdir/test4.txt",
2355  "$base/subdir2/subdir/test5.txt",
2356  "$base/subdir2/subdir/sub",
2357  "$base/subdir2/subdir/sub/test0.txt",
2358  "$base/subdir2/subdir/sub/120-px-file.txt",
2359  ];
2360 
2361  for ( $i = 0; $i < 25; $i++ ) {
2362  $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
2363  $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
2364  "Locking of files succeeded ($backendName) ($i)." );
2365  $this->assertEquals( true, $status->isOK(),
2366  "Locking of files succeeded with OK status ($backendName) ($i)." );
2367 
2368  $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH );
2369  $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
2370  "Locking of files succeeded ($backendName) ($i)." );
2371  $this->assertEquals( true, $status->isOK(),
2372  "Locking of files succeeded with OK status ($backendName) ($i)." );
2373 
2374  $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH );
2375  $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
2376  "Locking of files succeeded ($backendName) ($i)." );
2377  $this->assertEquals( true, $status->isOK(),
2378  "Locking of files succeeded with OK status ($backendName) ($i)." );
2379 
2380  $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX );
2381  $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
2382  "Locking of files succeeded ($backendName). ($i)" );
2383  $this->assertEquals( true, $status->isOK(),
2384  "Locking of files succeeded with OK status ($backendName) ($i)." );
2385 
2386  # # Flip the acquire/release ordering around ##
2387 
2388  $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH );
2389  $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
2390  "Locking of files succeeded ($backendName) ($i)." );
2391  $this->assertEquals( true, $status->isOK(),
2392  "Locking of files succeeded with OK status ($backendName) ($i)." );
2393 
2394  $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
2395  $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
2396  "Locking of files succeeded ($backendName) ($i)." );
2397  $this->assertEquals( true, $status->isOK(),
2398  "Locking of files succeeded with OK status ($backendName) ($i)." );
2399 
2400  $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX );
2401  $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
2402  "Locking of files succeeded ($backendName). ($i)" );
2403  $this->assertEquals( true, $status->isOK(),
2404  "Locking of files succeeded with OK status ($backendName) ($i)." );
2405 
2406  $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH );
2407  $this->assertEquals( print_r( [], true ), print_r( $status->getErrors(), true ),
2408  "Locking of files succeeded ($backendName) ($i)." );
2409  $this->assertEquals( true, $status->isOK(),
2410  "Locking of files succeeded with OK status ($backendName) ($i)." );
2411  }
2412 
2414  $sl = $this->backend->getScopedFileLocks( $paths, LockManager::LOCK_EX, $status );
2415  $this->assertInstanceOf( ScopedLock::class, $sl,
2416  "Scoped locking of files succeeded ($backendName)." );
2417  $this->assertEquals( [], $status->getErrors(),
2418  "Scoped locking of files succeeded ($backendName)." );
2419  $this->assertEquals( true, $status->isOK(),
2420  "Scoped locking of files succeeded with OK status ($backendName)." );
2421 
2422  ScopedLock::release( $sl );
2423  $this->assertEquals( null, $sl,
2424  "Scoped unlocking of files succeeded ($backendName)." );
2425  $this->assertEquals( [], $status->getErrors(),
2426  "Scoped unlocking of files succeeded ($backendName)." );
2427  $this->assertEquals( true, $status->isOK(),
2428  "Scoped unlocking of files succeeded with OK status ($backendName)." );
2429  }
2430 
2434  public function testGetContentType( $mimeCallback, $mimeFromString ) {
2435  global $IP;
2436 
2437  $be = TestingAccessWrapper::newFromObject( new MemoryFileBackend(
2438  [
2439  'name' => 'testing',
2440  'class' => MemoryFileBackend::class,
2441  'wikiId' => 'meow',
2442  'mimeCallback' => $mimeCallback
2443  ]
2444  ) );
2445 
2446  $dst = 'mwstore://testing/container/path/to/file_no_ext';
2447  $src = "$IP/tests/phpunit/data/media/srgb.jpg";
2448  $this->assertEquals( 'image/jpeg', $be->getContentType( $dst, null, $src ) );
2449  $this->assertEquals(
2450  $mimeFromString ? 'image/jpeg' : 'unknown/unknown',
2451  $be->getContentType( $dst, file_get_contents( $src ), null ) );
2452 
2453  $src = "$IP/tests/phpunit/data/media/Png-native-test.png";
2454  $this->assertEquals( 'image/png', $be->getContentType( $dst, null, $src ) );
2455  $this->assertEquals(
2456  $mimeFromString ? 'image/png' : 'unknown/unknown',
2457  $be->getContentType( $dst, file_get_contents( $src ), null ) );
2458  }
2459 
2460  public static function provider_testGetContentType() {
2461  return [
2462  [ null, false ],
2463  [ [ FileBackendGroup::singleton(), 'guessMimeInternal' ], true ]
2464  ];
2465  }
2466 
2467  public function testReadAffinity() {
2468  $be = TestingAccessWrapper::newFromObject(
2469  new FileBackendMultiWrite( [
2470  'name' => 'localtesting',
2471  'wikiId' => wfWikiID() . mt_rand(),
2472  'backends' => [
2473  [ // backend 0
2474  'name' => 'multitesting0',
2475  'class' => MemoryFileBackend::class,
2476  'isMultiMaster' => false,
2477  'readAffinity' => true
2478  ],
2479  [ // backend 1
2480  'name' => 'multitesting1',
2481  'class' => MemoryFileBackend::class,
2482  'isMultiMaster' => true
2483  ]
2484  ]
2485  ] )
2486  );
2487 
2488  $this->assertEquals(
2489  1,
2490  $be->getReadIndexFromParams( [ 'latest' => 1 ] ),
2491  'Reads with "latest" flag use backend 1'
2492  );
2493  $this->assertEquals(
2494  0,
2495  $be->getReadIndexFromParams( [ 'latest' => 0 ] ),
2496  'Reads without "latest" flag use backend 0'
2497  );
2498 
2499  $p = 'container/test-cont/file.txt';
2500  $be->backends[0]->quickCreate( [
2501  'dst' => "mwstore://multitesting0/$p", 'content' => 'cattitude' ] );
2502  $be->backends[1]->quickCreate( [
2503  'dst' => "mwstore://multitesting1/$p", 'content' => 'princess of power' ] );
2504 
2505  $this->assertEquals(
2506  'cattitude',
2507  $be->getFileContents( [ 'src' => "mwstore://localtesting/$p" ] ),
2508  "Non-latest read came from backend 0"
2509  );
2510  $this->assertEquals(
2511  'princess of power',
2512  $be->getFileContents( [ 'src' => "mwstore://localtesting/$p", 'latest' => 1 ] ),
2513  "Latest read came from backend1"
2514  );
2515  }
2516 
2517  public function testAsyncWrites() {
2518  $be = TestingAccessWrapper::newFromObject(
2519  new FileBackendMultiWrite( [
2520  'name' => 'localtesting',
2521  'wikiId' => wfWikiID() . mt_rand(),
2522  'backends' => [
2523  [ // backend 0
2524  'name' => 'multitesting0',
2525  'class' => MemoryFileBackend::class,
2526  'isMultiMaster' => false
2527  ],
2528  [ // backend 1
2529  'name' => 'multitesting1',
2530  'class' => MemoryFileBackend::class,
2531  'isMultiMaster' => true
2532  ]
2533  ],
2534  'replication' => 'async'
2535  ] )
2536  );
2537 
2538  $this->setMwGlobals( 'wgCommandLineMode', false );
2539 
2540  $p = 'container/test-cont/file.txt';
2541  $be->quickCreate( [
2542  'dst' => "mwstore://localtesting/$p", 'content' => 'cattitude' ] );
2543 
2544  $this->assertEquals(
2545  false,
2546  $be->backends[0]->getFileContents( [ 'src' => "mwstore://multitesting0/$p" ] ),
2547  "File not yet written to backend 0"
2548  );
2549  $this->assertEquals(
2550  'cattitude',
2551  $be->backends[1]->getFileContents( [ 'src' => "mwstore://multitesting1/$p" ] ),
2552  "File already written to backend 1"
2553  );
2554 
2556 
2557  $this->assertEquals(
2558  'cattitude',
2559  $be->backends[0]->getFileContents( [ 'src' => "mwstore://multitesting0/$p" ] ),
2560  "File now written to backend 0"
2561  );
2562  }
2563 
2564  public function testSanitizeOpHeaders() {
2565  $be = TestingAccessWrapper::newFromObject( new MemoryFileBackend( [
2566  'name' => 'localtesting',
2567  'wikiId' => wfWikiID()
2568  ] ) );
2569 
2570  $name = wfRandomString( 300 );
2571 
2572  $input = [
2573  'headers' => [
2574  'content-Disposition' => FileBackend::makeContentDisposition( 'inline', $name ),
2575  'Content-dUration' => 25.6,
2576  'X-LONG-VALUE' => str_pad( '0', 300 ),
2577  'CONTENT-LENGTH' => 855055,
2578  ]
2579  ];
2580  $expected = [
2581  'headers' => [
2582  'content-disposition' => FileBackend::makeContentDisposition( 'inline', $name ),
2583  'content-duration' => 25.6,
2584  'content-length' => 855055
2585  ]
2586  ];
2587 
2588  Wikimedia\suppressWarnings();
2589  $actual = $be->sanitizeOpHeaders( $input );
2590  Wikimedia\restoreWarnings();
2591 
2592  $this->assertEquals( $expected, $actual, "Header sanitized properly" );
2593  }
2594 
2595  // helper function
2596  private function listToArray( $iter ) {
2597  return is_array( $iter ) ? $iter : iterator_to_array( $iter );
2598  }
2599 
2600  // test helper wrapper for backend prepare() function
2601  private function prepare( array $params ) {
2602  return $this->backend->prepare( $params );
2603  }
2604 
2605  // test helper wrapper for backend prepare() function
2606  private function create( array $params ) {
2607  $params['op'] = 'create';
2608 
2609  return $this->backend->doQuickOperations( [ $params ] );
2610  }
2611 
2612  function tearDownFiles() {
2613  $containers = [ 'unittest-cont1', 'unittest-cont2', 'unittest-cont-bad' ];
2614  foreach ( $containers as $container ) {
2615  $this->deleteFiles( $container );
2616  }
2617  }
2618 
2619  private function deleteFiles( $container ) {
2620  $base = self::baseStorePath();
2621  $iter = $this->backend->getFileList( [ 'dir' => "$base/$container" ] );
2622  if ( $iter ) {
2623  foreach ( $iter as $file ) {
2624  $this->backend->quickDelete( [ 'src' => "$base/$container/$file" ] );
2625  }
2626  // free the directory, to avoid Permission denied under windows on rmdir
2627  unset( $iter );
2628  }
2629  $this->backend->clean( [ 'dir' => "$base/$container", 'recursive' => 1 ] );
2630  }
2631 
2633  if ( $this->backend instanceof FileBackendMultiWrite ) {
2634  $status = $this->backend->consistencyCheck( $paths );
2635  $this->assertGoodStatus( $status, "Files synced: " . implode( ',', $paths ) );
2636  }
2637  }
2638 
2640  $this->assertEquals( print_r( [], 1 ), print_r( $status->getErrors(), 1 ), $msg );
2641  }
2642 }
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
static splitStoragePath( $storagePath)
Split a storage path into a backend name, a container name, and a relative file path.
doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus)
static provider_testParentStoragePath()
FSFileBackend $singleBackend
if(is_array( $mode)) switch( $mode) $input
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
$IP
Definition: WebStart.php:41
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
static parentStoragePath( $storagePath)
Get the parent storage directory of a storage path.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Definition: router.php:42
testGetFileContents( $source, $content)
provider_testGetFileContents
static provider_testGetLocalReference()
static provider_testGetFileContents()
doTestGetFileHttpUrl( $source, $content)
assertHasHeaders(array $headers, array $attr)
testGetFileHttpUrl( $source, $content)
provider_testGetFileHttpUrl
$wgFileBackends
File backend structure configuration.
const ATTR_HEADERS
Bitfield flags for supported features.
testGetFileStat( $path, $content, $alreadyExists)
provider_testGetFileStat
static provider_testExtensionFromPath()
assertBackendPathsConsistent(array $paths)
$source
testDelete( $op, $withSource, $okStatus)
provider_testDelete
testStore( $op)
provider_testStore
static provider_testCopy()
static extensionFromPath( $path, $case='lowercase')
Get the final extension from a storage or FS path.
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
static normalizeStoragePath( $storagePath)
Normalize a storage path by cleaning up directory separators.
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition: hooks.txt:1799
doTestDelete( $op, $withSource, $okStatus)
static getPropsFromPath( $path, $ext=true)
Get an associative array containing information about a file in the local filesystem.
Definition: FSFile.php:202
doTestGetLocalReference( $source, $content)
getNewTempFile()
Obtains a new temporary file name.
FileBackend $backend
static provider_testConcatenate()
doTestGetFileStat( $path, $content, $alreadyExists)
testCopy( $op)
provider_testCopy
static provider_normalizeStoragePath()
static request( $method, $url, array $options=[], $caller=__METHOD__)
Perform an HTTP request.
Definition: Http.php:61
doTestGetLocalCopy( $source, $content)
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:1982
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition: hooks.txt:1263
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
static provider_testCreate()
provider_testCreate
This program is free software; you can redistribute it and/or modify it under the terms of the GNU Ge...
wfTempDir()
Tries to get the system directory for temporary files.
static factory( $prefix, $extension='', $tmpDirectory=null)
Make a new temporary file on the file system.
Definition: TempFSFile.php:55
static provider_testGetLocalCopy()
wfRandomString( $length=32)
Get a random string containing a number of pseudo-random hex characters.
const LOCK_EX
Definition: LockManager.php:69
$res
Definition: database.txt:21
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:81
static provider_testIsStoragePath()
testConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus)
provider_testConcatenate
static provider_testMove()
static provider_testGetFileStat()
create(array $params)
doTestPrepareAndClean( $path, $isOK)
static provider_testDelete()
FileRepo FileBackend medium.
$params
static provider_testPrepareAndClean()
doTestGetFileContents( $source, $content)
const LOCK_SH
Lock types; stronger locks have higher values.
Definition: LockManager.php:67
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
testGetLocalReference( $source, $content)
provider_testGetLocalReference
prepare(array $params)
static provider_testDescribe()
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
assertGoodStatus(StatusValue $status, $msg)
doTestStreamFile( $path, $content)
testDescribe( $op, $withSource, $okStatus)
provider_testDescribe
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown...
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
static provider_testSplitStoragePath()
static provider_testStore()
testMove( $op)
provider_testMove
testParentStoragePath( $path, $res)
provider_testParentStoragePath
testStreamFile( $path, $content, $alreadyExists)
provider_testGetFileStat
static provider_testStreamFile()
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
static singleton( $domain=false)
static doUpdates( $mode='run', $stage=self::ALL)
Do any deferred updates and clear the list.
doTestDescribe( $op, $withSource, $okStatus)
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
Simulation of a backend storage in memory.
testGetContentType( $mimeCallback, $mimeFromString)
provider_testGetContentType
Proxy backend that mirrors writes to several internal backends.
getErrors()
Get the list of errors.
static provider_testGetContentType()
testNormalizeStoragePath( $path, $res)
provider_normalizeStoragePath
testGetLocalCopy( $source, $content)
provider_testGetLocalCopy
getNewTempDirectory()
obtains a new temporary directory
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
FileBackendMultiWrite $multiBackend
testSplitStoragePath( $path, $res)
provider_testSplitStoragePath
doTestCreate( $op, $alreadyExists, $okStatus, $newSize)
array $tmpFiles
Holds the paths of temporary files/directories created through getNewTempFile, and getNewTempDirector...
Class for a file system (FS) based file backend.
$content
Definition: pageupdater.txt:72
testIsStoragePath( $path, $isStorePath)
provider_testIsStoragePath
static release(ScopedLock &$lock=null)
Release a scoped lock and set any errors in the attatched StatusValue object.
Definition: ScopedLock.php:91
deleteFiles( $container)
static makeContentDisposition( $type, $filename='')
Build a Content-Disposition header value per RFC 6266.
static factory(array $config, $backend)
Create an appropriate FileJournal object from config.
Definition: FileJournal.php:62
testCreate( $op, $alreadyExists, $okStatus, $newSize)
provider_testCreate
testPrepareAndClean( $path, $isOK)
provider_testPrepareAndClean
testExtensionFromPath( $path, $res)
provider_testExtensionFromPath
static provider_testGetFileHttpUrl()