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