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