See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 39 and 401] [Versions 401 and 402] [Versions 401 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace core; 18 19 use file_system_filedir; 20 21 defined('MOODLE_INTERNAL') || die(); 22 23 global $CFG; 24 require_once($CFG->libdir . '/filestorage/file_system.php'); 25 require_once($CFG->libdir . '/filestorage/file_system_filedir.php'); 26 27 /** 28 * Unit tests for file_system_filedir. 29 * 30 * @package core 31 * @category test 32 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk> 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 * @coversDefaultClass \file_system_filedir 35 */ 36 class file_system_filedir_test extends \advanced_testcase { 37 38 /** 39 * Shared test setUp. 40 */ 41 public function setUp(): void { 42 // Reset the file storage so that subsequent fetches to get_file_storage are called after 43 // configuration is prepared. 44 get_file_storage(true); 45 } 46 47 /** 48 * Shared teset tearDown. 49 */ 50 public function tearDown(): void { 51 // Reset the file storage so that subsequent tests will use the standard file storage. 52 get_file_storage(true); 53 } 54 55 /** 56 * Helper function to help setup and configure the virtual file system stream. 57 * 58 * @param array $filedir Directory structure and content of the filedir 59 * @param array $trashdir Directory structure and content of the sourcedir 60 * @param array $sourcedir Directory structure and content of a directory used for source files for tests 61 * @return \org\bovigo\vfs\vfsStream 62 */ 63 protected function setup_vfile_root($filedir = [], $trashdir = [], $sourcedir = null) { 64 global $CFG; 65 $this->resetAfterTest(); 66 67 $content = []; 68 if ($filedir !== null) { 69 $content['filedir'] = $filedir; 70 } 71 72 if ($trashdir !== null) { 73 $content['trashdir'] = $trashdir; 74 } 75 76 if ($sourcedir !== null) { 77 $content['sourcedir'] = $sourcedir; 78 } 79 80 $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, $content); 81 82 $CFG->filedir = \org\bovigo\vfs\vfsStream::url('root/filedir'); 83 $CFG->trashdir = \org\bovigo\vfs\vfsStream::url('root/trashdir'); 84 85 return $vfileroot; 86 } 87 88 /** 89 * Helper to create a stored file objectw with the given supplied content. 90 * 91 * @param string $filecontent The content of the mocked file 92 * @param string $filename The file name to use in the stored_file 93 * @param array $mockedmethods A list of methods you intend to override 94 * If no methods are specified, only abstract functions are mocked. 95 * @return stored_file 96 */ 97 protected function get_stored_file($filecontent, $filename = null, $mockedmethods = []) { 98 $contenthash = \file_storage::hash_from_string($filecontent); 99 if (empty($filename)) { 100 $filename = $contenthash; 101 } 102 103 $file = $this->getMockBuilder(\stored_file::class) 104 ->onlyMethods($mockedmethods) 105 ->setConstructorArgs([ 106 get_file_storage(), 107 (object) [ 108 'contenthash' => $contenthash, 109 'filesize' => strlen($filecontent), 110 'filename' => $filename, 111 ] 112 ]) 113 ->getMock(); 114 115 return $file; 116 } 117 118 /** 119 * Get a testable mock of the file_system_filedir class. 120 * 121 * @param array $mockedmethods A list of methods you intend to override 122 * If no methods are specified, only abstract functions are mocked. 123 * @return file_system 124 */ 125 protected function get_testable_mock($mockedmethods = []) { 126 $fs = $this->getMockBuilder(file_system_filedir::class) 127 ->onlyMethods($mockedmethods) 128 ->getMock(); 129 130 return $fs; 131 } 132 133 /** 134 * Ensure that an appropriate error is shown when the filedir directory 135 * is not writable. 136 * 137 * @covers ::__construct 138 */ 139 public function test_readonly_filesystem_filedir() { 140 $this->resetAfterTest(); 141 142 // Setup the filedir but remove permissions. 143 $vfileroot = $this->setup_vfile_root(null); 144 145 // Make the target path readonly. 146 $vfileroot->chmod(0444) 147 ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2); 148 149 // This should generate an exception. 150 $this->expectException('file_exception'); 151 $this->expectExceptionMessageMatches( 152 '/Cannot create local file pool directories. Please verify permissions in dataroot./'); 153 154 new file_system_filedir(); 155 } 156 157 /** 158 * Ensure that an appropriate error is shown when the trash directory 159 * is not writable. 160 * 161 * @covers ::__construct 162 */ 163 public function test_readonly_filesystem_trashdir() { 164 $this->resetAfterTest(); 165 166 // Setup the trashdir but remove permissions. 167 $vfileroot = $this->setup_vfile_root([], null); 168 169 // Make the target path readonly. 170 $vfileroot->chmod(0444) 171 ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2); 172 173 // This should generate an exception. 174 $this->expectException('file_exception'); 175 $this->expectExceptionMessageMatches( 176 '/Cannot create local file pool directories. Please verify permissions in dataroot./'); 177 178 new file_system_filedir(); 179 } 180 181 /** 182 * Test that the standard Moodle warning message is put into the filedir. 183 * 184 * @covers ::__construct 185 */ 186 public function test_warnings_put_in_place() { 187 $this->resetAfterTest(); 188 189 $vfileroot = $this->setup_vfile_root(null); 190 191 new file_system_filedir(); 192 $this->assertTrue($vfileroot->hasChild('filedir/warning.txt')); 193 $this->assertEquals( 194 'This directory contains the content of uploaded files and is controlled by Moodle code. ' . 195 'Do not manually move, change or rename any of the files and subdirectories here.', 196 $vfileroot->getChild('filedir/warning.txt')->getContent() 197 ); 198 } 199 200 /** 201 * Ensure that the default implementation of get_remote_path_from_hash 202 * simply calls get_local_path_from_hash. 203 * 204 * @covers ::get_remote_path_from_hash 205 */ 206 public function test_get_remote_path_from_hash() { 207 $filecontent = 'example content'; 208 $contenthash = \file_storage::hash_from_string($filecontent); 209 $expectedresult = (object) []; 210 211 $fs = $this->get_testable_mock([ 212 'get_local_path_from_hash', 213 ]); 214 215 $fs->expects($this->once()) 216 ->method('get_local_path_from_hash') 217 ->with($this->equalTo($contenthash), $this->equalTo(false)) 218 ->willReturn($expectedresult); 219 220 $method = new \ReflectionMethod(file_system_filedir::class, 'get_remote_path_from_hash'); 221 $method->setAccessible(true); 222 $result = $method->invokeArgs($fs, [$contenthash]); 223 224 $this->assertEquals($expectedresult, $result); 225 } 226 227 /** 228 * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and 229 * a failed recovery. 230 * 231 * @covers ::get_local_path_from_storedfile 232 */ 233 public function test_get_local_path_from_storedfile_with_recovery() { 234 $filecontent = 'example content'; 235 $file = $this->get_stored_file($filecontent); 236 $fs = $this->get_testable_mock([ 237 'get_local_path_from_hash', 238 'recover_file', 239 ]); 240 $filepath = '/path/to/nonexistent/file'; 241 242 $fs->method('get_local_path_from_hash') 243 ->willReturn($filepath); 244 245 $fs->expects($this->once()) 246 ->method('recover_file') 247 ->with($this->equalTo($file)); 248 249 $file = $this->get_stored_file('example content'); 250 $result = $fs->get_local_path_from_storedfile($file, true); 251 252 $this->assertEquals($filepath, $result); 253 } 254 255 /** 256 * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and 257 * a failed recovery. 258 * 259 * @covers ::get_local_path_from_storedfile 260 */ 261 public function test_get_local_path_from_storedfile_without_recovery() { 262 $filecontent = 'example content'; 263 $file = $this->get_stored_file($filecontent); 264 $fs = $this->get_testable_mock([ 265 'get_local_path_from_hash', 266 'recover_file', 267 ]); 268 $filepath = '/path/to/nonexistent/file'; 269 270 $fs->method('get_local_path_from_hash') 271 ->willReturn($filepath); 272 273 $fs->expects($this->never()) 274 ->method('recover_file'); 275 276 $file = $this->get_stored_file('example content'); 277 $result = $fs->get_local_path_from_storedfile($file, false); 278 279 $this->assertEquals($filepath, $result); 280 } 281 282 /** 283 * Test that the correct path is generated for the supplied content 284 * hashes. 285 * 286 * @dataProvider contenthash_dataprovider 287 * @param string $hash contenthash to test 288 * @param string $hashdir Expected format of content directory 289 * 290 * @covers ::get_fulldir_from_hash 291 */ 292 public function test_get_fulldir_from_hash($hash, $hashdir) { 293 global $CFG; 294 295 $fs = new file_system_filedir(); 296 $method = new \ReflectionMethod(file_system_filedir::class, 'get_fulldir_from_hash'); 297 $method->setAccessible(true); 298 $result = $method->invokeArgs($fs, array($hash)); 299 300 $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir); 301 $this->assertEquals($expectedpath, $result); 302 } 303 304 /** 305 * Test that the correct path is generated for the supplied content 306 * hashes when used with a stored_file. 307 * 308 * @dataProvider contenthash_dataprovider 309 * @param string $hash contenthash to test 310 * @param string $hashdir Expected format of content directory 311 * 312 * @covers ::get_fulldir_from_storedfile 313 */ 314 public function test_get_fulldir_from_storedfile($hash, $hashdir) { 315 global $CFG; 316 317 $file = $this->getMockBuilder('stored_file') 318 ->disableOriginalConstructor() 319 ->onlyMethods([ 320 'sync_external_file', 321 'get_contenthash', 322 ]) 323 ->getMock(); 324 325 $file->method('get_contenthash')->willReturn($hash); 326 327 $fs = new file_system_filedir(); 328 $method = new \ReflectionMethod('file_system_filedir', 'get_fulldir_from_storedfile'); 329 $method->setAccessible(true); 330 $result = $method->invokeArgs($fs, array($file)); 331 332 $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir); 333 $this->assertEquals($expectedpath, $result); 334 } 335 336 /** 337 * Test that the correct content directory is generated for the supplied 338 * content hashes. 339 * 340 * @dataProvider contenthash_dataprovider 341 * @param string $hash contenthash to test 342 * @param string $hashdir Expected format of content directory 343 * 344 * @covers ::get_contentdir_from_hash 345 */ 346 public function test_get_contentdir_from_hash($hash, $hashdir) { 347 $method = new \ReflectionMethod(file_system_filedir::class, 'get_contentdir_from_hash'); 348 $method->setAccessible(true); 349 350 $fs = new file_system_filedir(); 351 $result = $method->invokeArgs($fs, array($hash)); 352 353 $this->assertEquals($hashdir, $result); 354 } 355 356 /** 357 * Test that the correct content path is generated for the supplied 358 * content hashes. 359 * 360 * @dataProvider contenthash_dataprovider 361 * @param string $hash contenthash to test 362 * @param string $hashdir Expected format of content directory 363 * 364 * @covers ::get_contentpath_from_hash 365 */ 366 public function test_get_contentpath_from_hash($hash, $hashdir) { 367 $method = new \ReflectionMethod(file_system_filedir::class, 'get_contentpath_from_hash'); 368 $method->setAccessible(true); 369 370 $fs = new file_system_filedir(); 371 $result = $method->invokeArgs($fs, array($hash)); 372 373 $expectedpath = sprintf('%s/%s', $hashdir, $hash); 374 $this->assertEquals($expectedpath, $result); 375 } 376 377 /** 378 * Test that the correct trash path is generated for the supplied 379 * content hashes. 380 * 381 * @dataProvider contenthash_dataprovider 382 * @param string $hash contenthash to test 383 * @param string $hashdir Expected format of content directory 384 * 385 * @covers ::get_trash_fullpath_from_hash 386 */ 387 public function test_get_trash_fullpath_from_hash($hash, $hashdir) { 388 global $CFG; 389 390 $fs = new file_system_filedir(); 391 $method = new \ReflectionMethod(file_system_filedir::class, 'get_trash_fullpath_from_hash'); 392 $method->setAccessible(true); 393 $result = $method->invokeArgs($fs, array($hash)); 394 395 $expectedpath = sprintf('%s/trashdir/%s/%s', $CFG->dataroot, $hashdir, $hash); 396 $this->assertEquals($expectedpath, $result); 397 } 398 399 /** 400 * Test that the correct trash directory is generated for the supplied 401 * content hashes. 402 * 403 * @dataProvider contenthash_dataprovider 404 * @param string $hash contenthash to test 405 * @param string $hashdir Expected format of content directory 406 * 407 * @covers ::get_trash_fulldir_from_hash 408 */ 409 public function test_get_trash_fulldir_from_hash($hash, $hashdir) { 410 global $CFG; 411 412 $fs = new file_system_filedir(); 413 $method = new \ReflectionMethod(file_system_filedir::class, 'get_trash_fulldir_from_hash'); 414 $method->setAccessible(true); 415 $result = $method->invokeArgs($fs, array($hash)); 416 417 $expectedpath = sprintf('%s/trashdir/%s', $CFG->dataroot, $hashdir); 418 $this->assertEquals($expectedpath, $result); 419 } 420 421 /** 422 * Ensure that copying a file to a target from a stored_file works as anticipated. 423 * 424 * @covers ::copy_content_from_storedfile 425 */ 426 public function test_copy_content_from_storedfile() { 427 $this->resetAfterTest(); 428 global $CFG; 429 430 $filecontent = 'example content'; 431 $contenthash = \file_storage::hash_from_string($filecontent); 432 $filedircontent = [ 433 $contenthash => $filecontent, 434 ]; 435 $vfileroot = $this->setup_vfile_root($filedircontent, [], []); 436 437 $fs = $this->getMockBuilder(file_system_filedir::class) 438 ->disableOriginalConstructor() 439 ->onlyMethods([ 440 'get_local_path_from_storedfile', 441 ]) 442 ->getMock(); 443 444 $file = $this->getMockBuilder(\stored_file::class) 445 ->disableOriginalConstructor() 446 ->getMock(); 447 448 $sourcefile = \org\bovigo\vfs\vfsStream::url('root/filedir/' . $contenthash); 449 $fs->method('get_local_path_from_storedfile')->willReturn($sourcefile); 450 451 $targetfile = \org\bovigo\vfs\vfsStream::url('root/targetfile'); 452 $CFG->preventfilelocking = true; 453 $result = $fs->copy_content_from_storedfile($file, $targetfile); 454 455 $this->assertTrue($result); 456 $this->assertEquals($filecontent, $vfileroot->getChild('targetfile')->getContent()); 457 } 458 459 /** 460 * Ensure that content recovery works. 461 * 462 * @covers ::recover_file 463 */ 464 public function test_recover_file() { 465 $this->resetAfterTest(); 466 467 // Setup the filedir. 468 // This contains a virtual file which has a cache mismatch. 469 $filecontent = 'example content'; 470 $contenthash = \file_storage::hash_from_string($filecontent); 471 472 $trashdircontent = [ 473 '0f' => [ 474 'f3' => [ 475 $contenthash => $filecontent, 476 ], 477 ], 478 ]; 479 480 $vfileroot = $this->setup_vfile_root([], $trashdircontent); 481 482 $file = new \stored_file(get_file_storage(), (object) [ 483 'contenthash' => $contenthash, 484 'filesize' => strlen($filecontent), 485 ]); 486 487 $fs = new file_system_filedir(); 488 $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file'); 489 $method->setAccessible(true); 490 $result = $method->invokeArgs($fs, array($file)); 491 492 // Test the output. 493 $this->assertTrue($result); 494 495 $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent()); 496 497 } 498 499 /** 500 * Ensure that content recovery works. 501 * 502 * @covers ::recover_file 503 */ 504 public function test_recover_file_already_present() { 505 $this->resetAfterTest(); 506 507 // Setup the filedir. 508 // This contains a virtual file which has a cache mismatch. 509 $filecontent = 'example content'; 510 $contenthash = \file_storage::hash_from_string($filecontent); 511 512 $filedircontent = $trashdircontent = [ 513 '0f' => [ 514 'f3' => [ 515 $contenthash => $filecontent, 516 ], 517 ], 518 ]; 519 520 $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent); 521 522 $file = new \stored_file(get_file_storage(), (object) [ 523 'contenthash' => $contenthash, 524 'filesize' => strlen($filecontent), 525 ]); 526 527 $fs = new file_system_filedir(); 528 $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file'); 529 $method->setAccessible(true); 530 $result = $method->invokeArgs($fs, array($file)); 531 532 // Test the output. 533 $this->assertTrue($result); 534 535 $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent()); 536 } 537 538 /** 539 * Ensure that content recovery works. 540 * 541 * @covers ::recover_file 542 */ 543 public function test_recover_file_size_mismatch() { 544 $this->resetAfterTest(); 545 546 // Setup the filedir. 547 // This contains a virtual file which has a cache mismatch. 548 $filecontent = 'example content'; 549 $contenthash = \file_storage::hash_from_string($filecontent); 550 551 $trashdircontent = [ 552 '0f' => [ 553 'f3' => [ 554 $contenthash => $filecontent, 555 ], 556 ], 557 ]; 558 $vfileroot = $this->setup_vfile_root([], $trashdircontent); 559 560 $file = new \stored_file(get_file_storage(), (object) [ 561 'contenthash' => $contenthash, 562 'filesize' => strlen($filecontent) + 1, 563 ]); 564 565 $fs = new file_system_filedir(); 566 $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file'); 567 $method->setAccessible(true); 568 $result = $method->invokeArgs($fs, array($file)); 569 570 // Test the output. 571 $this->assertFalse($result); 572 $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash)); 573 } 574 575 /** 576 * Ensure that content recovery works. 577 * 578 * @covers ::recover_file 579 */ 580 public function test_recover_file_has_mismatch() { 581 $this->resetAfterTest(); 582 583 // Setup the filedir. 584 // This contains a virtual file which has a cache mismatch. 585 $filecontent = 'example content'; 586 $contenthash = \file_storage::hash_from_string($filecontent); 587 588 $trashdircontent = [ 589 '0f' => [ 590 'f3' => [ 591 $contenthash => $filecontent, 592 ], 593 ], 594 ]; 595 $vfileroot = $this->setup_vfile_root([], $trashdircontent); 596 597 $file = new \stored_file(get_file_storage(), (object) [ 598 'contenthash' => $contenthash . " different", 599 'filesize' => strlen($filecontent), 600 ]); 601 602 $fs = new file_system_filedir(); 603 $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file'); 604 $method->setAccessible(true); 605 $result = $method->invokeArgs($fs, array($file)); 606 607 // Test the output. 608 $this->assertFalse($result); 609 $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash)); 610 } 611 612 /** 613 * Ensure that content recovery works when the content file is in the 614 * alt trash directory. 615 * 616 * @covers ::recover_file 617 */ 618 public function test_recover_file_alttrash() { 619 $this->resetAfterTest(); 620 621 // Setup the filedir. 622 // This contains a virtual file which has a cache mismatch. 623 $filecontent = 'example content'; 624 $contenthash = \file_storage::hash_from_string($filecontent); 625 626 $trashdircontent = [ 627 $contenthash => $filecontent, 628 ]; 629 $vfileroot = $this->setup_vfile_root([], $trashdircontent); 630 631 $file = new \stored_file(get_file_storage(), (object) [ 632 'contenthash' => $contenthash, 633 'filesize' => strlen($filecontent), 634 ]); 635 636 $fs = new file_system_filedir(); 637 $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file'); 638 $method->setAccessible(true); 639 $result = $method->invokeArgs($fs, array($file)); 640 641 // Test the output. 642 $this->assertTrue($result); 643 644 $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent()); 645 } 646 647 /** 648 * Test that an appropriate error message is generated when adding a 649 * file to the pool when the pool directory structure is not writable. 650 * 651 * @covers ::recover_file 652 */ 653 public function test_recover_file_contentdir_readonly() { 654 $this->resetAfterTest(); 655 656 $filecontent = 'example content'; 657 $contenthash = \file_storage::hash_from_string($filecontent); 658 $filedircontent = [ 659 '0f' => [], 660 ]; 661 $trashdircontent = [ 662 $contenthash => $filecontent, 663 ]; 664 $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent); 665 666 // Make the target path readonly. 667 $vfileroot->getChild('filedir/0f') 668 ->chmod(0444) 669 ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2); 670 671 $file = new \stored_file(get_file_storage(), (object) [ 672 'contenthash' => $contenthash, 673 'filesize' => strlen($filecontent), 674 ]); 675 676 $fs = new file_system_filedir(); 677 $method = new \ReflectionMethod(file_system_filedir::class, 'recover_file'); 678 $method->setAccessible(true); 679 $result = $method->invokeArgs($fs, array($file)); 680 681 // Test the output. 682 $this->assertFalse($result); 683 } 684 685 /** 686 * Test adding a file to the pool. 687 * 688 * @covers ::add_file_from_path 689 */ 690 public function test_add_file_from_path() { 691 $this->resetAfterTest(); 692 global $CFG; 693 694 // Setup the filedir. 695 // This contains a virtual file which has a cache mismatch. 696 $filecontent = 'example content'; 697 $contenthash = \file_storage::hash_from_string($filecontent); 698 $sourcedircontent = [ 699 'file' => $filecontent, 700 ]; 701 702 $vfileroot = $this->setup_vfile_root([], [], $sourcedircontent); 703 704 // Note, the vfs file system does not support locks - prevent file locking here. 705 $CFG->preventfilelocking = true; 706 707 // Attempt to add the file to the file pool. 708 $fs = new file_system_filedir(); 709 $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file'); 710 $result = $fs->add_file_from_path($sourcefile); 711 712 // Test the output. 713 $this->assertEquals($contenthash, $result[0]); 714 $this->assertEquals(\core_text::strlen($filecontent), $result[1]); 715 $this->assertTrue($result[2]); 716 717 $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent()); 718 } 719 720 /** 721 * Test that an appropriate error message is generated when adding an 722 * unavailable file to the pool is attempted. 723 * 724 * @covers ::add_file_from_path 725 */ 726 public function test_add_file_from_path_file_unavailable() { 727 $this->resetAfterTest(); 728 729 // Setup the filedir. 730 $vfileroot = $this->setup_vfile_root(); 731 732 $this->expectException('file_exception'); 733 $this->expectExceptionMessageMatches( 734 '/Cannot read file\. Either the file does not exist or there is a permission problem\./'); 735 736 $fs = new file_system_filedir(); 737 $fs->add_file_from_path(\org\bovigo\vfs\vfsStream::url('filedir/file')); 738 } 739 740 /** 741 * Test that an appropriate error message is generated when specifying 742 * the wrong contenthash when adding a file to the pool. 743 * 744 * @covers ::add_file_from_path 745 */ 746 public function test_add_file_from_path_mismatched_hash() { 747 $this->resetAfterTest(); 748 749 $filecontent = 'example content'; 750 $contenthash = \file_storage::hash_from_string($filecontent); 751 $sourcedir = [ 752 'file' => $filecontent, 753 ]; 754 $vfileroot = $this->setup_vfile_root([], [], $sourcedir); 755 756 $fs = new file_system_filedir(); 757 $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file'); 758 $fs->add_file_from_path($filepath, 'eee4943847a35a4b6942c6f96daafde06bcfdfab'); 759 $this->assertDebuggingCalled("Invalid contenthash submitted for file $filepath"); 760 } 761 762 /** 763 * Test that an appropriate error message is generated when an existing 764 * file in the pool has the wrong contenthash 765 * 766 * @covers ::add_file_from_path 767 */ 768 public function test_add_file_from_path_existing_content_invalid() { 769 $this->resetAfterTest(); 770 771 $filecontent = 'example content'; 772 $contenthash = \file_storage::hash_from_string($filecontent); 773 $filedircontent = [ 774 '0f' => [ 775 'f3' => [ 776 // This contains a virtual file which has a cache mismatch. 777 '0ff30941ca5acd879fd809e8c937d9f9e6dd1615' => 'different example content', 778 ], 779 ], 780 ]; 781 $sourcedir = [ 782 'file' => $filecontent, 783 ]; 784 $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir); 785 786 // Check that we hit the jackpot. 787 $fs = new file_system_filedir(); 788 $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file'); 789 $result = $fs->add_file_from_path($filepath); 790 791 // We provided a bad hash. Check that the file was replaced. 792 $this->assertDebuggingCalled("Replacing invalid content file $contenthash"); 793 794 // Test the output. 795 $this->assertEquals($contenthash, $result[0]); 796 $this->assertEquals(\core_text::strlen($filecontent), $result[1]); 797 $this->assertFalse($result[2]); 798 799 // Fetch the new file structure. 800 $structure = \org\bovigo\vfs\vfsStream::inspect( 801 new \org\bovigo\vfs\visitor\vfsStreamStructureVisitor() 802 )->getStructure(); 803 804 $this->assertEquals($filecontent, $structure['root']['filedir']['0f']['f3'][$contenthash]); 805 } 806 807 /** 808 * Test that an appropriate error message is generated when adding a 809 * file to the pool when the pool directory structure is not writable. 810 * 811 * @covers ::add_file_from_path 812 */ 813 public function test_add_file_from_path_existing_cannot_write_hashpath() { 814 $this->resetAfterTest(); 815 816 $filecontent = 'example content'; 817 $contenthash = \file_storage::hash_from_string($filecontent); 818 $filedircontent = [ 819 '0f' => [], 820 ]; 821 $sourcedir = [ 822 'file' => $filecontent, 823 ]; 824 $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir); 825 826 // Make the target path readonly. 827 $vfileroot->getChild('filedir/0f') 828 ->chmod(0444) 829 ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2); 830 831 $this->expectException('file_exception'); 832 $this->expectExceptionMessageMatches( 833 "/Cannot create local file pool directories. Please verify permissions in dataroot./"); 834 835 // Attempt to add the file to the file pool. 836 $fs = new file_system_filedir(); 837 $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file'); 838 $fs->add_file_from_path($sourcefile); 839 } 840 841 /** 842 * Test adding a string to the pool. 843 * 844 * @covers ::add_file_from_string 845 */ 846 public function test_add_file_from_string() { 847 $this->resetAfterTest(); 848 global $CFG; 849 850 $filecontent = 'example content'; 851 $contenthash = \file_storage::hash_from_string($filecontent); 852 $vfileroot = $this->setup_vfile_root(); 853 854 // Note, the vfs file system does not support locks - prevent file locking here. 855 $CFG->preventfilelocking = true; 856 857 // Attempt to add the file to the file pool. 858 $fs = new file_system_filedir(); 859 $result = $fs->add_file_from_string($filecontent); 860 861 // Test the output. 862 $this->assertEquals($contenthash, $result[0]); 863 $this->assertEquals(\core_text::strlen($filecontent), $result[1]); 864 $this->assertTrue($result[2]); 865 } 866 867 /** 868 * Test that an appropriate error message is generated when adding a 869 * string to the pool when the pool directory structure is not writable. 870 * 871 * @covers ::add_file_from_string 872 */ 873 public function test_add_file_from_string_existing_cannot_write_hashpath() { 874 $this->resetAfterTest(); 875 876 $filecontent = 'example content'; 877 $contenthash = \file_storage::hash_from_string($filecontent); 878 879 $filedircontent = [ 880 '0f' => [], 881 ]; 882 $vfileroot = $this->setup_vfile_root($filedircontent); 883 884 // Make the target path readonly. 885 $vfileroot->getChild('filedir/0f') 886 ->chmod(0444) 887 ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2); 888 889 $this->expectException('file_exception'); 890 $this->expectExceptionMessageMatches( 891 "/Cannot create local file pool directories. Please verify permissions in dataroot./"); 892 893 // Attempt to add the file to the file pool. 894 $fs = new file_system_filedir(); 895 $fs->add_file_from_string($filecontent); 896 } 897 898 /** 899 * Test adding a string to the pool when an item with the same 900 * contenthash is already present. 901 * 902 * @covers ::add_file_from_string 903 */ 904 public function test_add_file_from_string_existing_matches() { 905 $this->resetAfterTest(); 906 global $CFG; 907 908 $filecontent = 'example content'; 909 $contenthash = \file_storage::hash_from_string($filecontent); 910 $filedircontent = [ 911 '0f' => [ 912 'f3' => [ 913 $contenthash => $filecontent, 914 ], 915 ], 916 ]; 917 918 $vfileroot = $this->setup_vfile_root($filedircontent); 919 920 // Note, the vfs file system does not support locks - prevent file locking here. 921 $CFG->preventfilelocking = true; 922 923 // Attempt to add the file to the file pool. 924 $fs = new file_system_filedir(); 925 $result = $fs->add_file_from_string($filecontent); 926 927 // Test the output. 928 $this->assertEquals($contenthash, $result[0]); 929 $this->assertEquals(\core_text::strlen($filecontent), $result[1]); 930 $this->assertFalse($result[2]); 931 } 932 933 /** 934 * Test the cleanup of deleted files when there are no files to delete. 935 * 936 * @covers ::remove_file 937 */ 938 public function test_remove_file_missing() { 939 $this->resetAfterTest(); 940 941 $filecontent = 'example content'; 942 $contenthash = \file_storage::hash_from_string($filecontent); 943 $vfileroot = $this->setup_vfile_root(); 944 945 $fs = new file_system_filedir(); 946 $fs->remove_file($contenthash); 947 948 $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash)); 949 // No file to move to trash, so the trash path will also be empty. 950 $this->assertFalse($vfileroot->hasChild('trashdir/0f')); 951 $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3')); 952 $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash)); 953 } 954 955 /** 956 * Test the cleanup of deleted files when a file already exists in the 957 * trash for that path. 958 * 959 * @covers ::remove_file 960 */ 961 public function test_remove_file_existing_trash() { 962 $this->resetAfterTest(); 963 964 $filecontent = 'example content'; 965 $contenthash = \file_storage::hash_from_string($filecontent); 966 967 $filedircontent = $trashdircontent = [ 968 '0f' => [ 969 'f3' => [ 970 $contenthash => $filecontent, 971 ], 972 ], 973 ]; 974 $trashdircontent['0f']['f3'][$contenthash] .= 'different'; 975 $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent); 976 977 $fs = new file_system_filedir(); 978 $fs->remove_file($contenthash); 979 980 $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash)); 981 $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash)); 982 $this->assertNotEquals($filecontent, $vfileroot->getChild('trashdir/0f/f3/' . $contenthash)->getContent()); 983 } 984 985 /** 986 * Ensure that remove_file does nothing with an empty file. 987 * 988 * @covers ::remove_file 989 */ 990 public function test_remove_file_empty() { 991 $this->resetAfterTest(); 992 global $DB; 993 994 $DB = $this->getMockBuilder(\moodle_database::class) 995 ->onlyMethods(['record_exists']) 996 ->getMockForAbstractClass(); 997 998 $DB->expects($this->never()) 999 ->method('record_exists'); 1000 1001 $fs = new file_system_filedir(); 1002 1003 $result = $fs->remove_file(\file_storage::hash_from_string('')); 1004 $this->assertNull($result); 1005 } 1006 1007 /** 1008 * Ensure that remove_file does nothing when a file is still 1009 * in use. 1010 * 1011 * @covers ::remove_file 1012 */ 1013 public function test_remove_file_in_use() { 1014 $this->resetAfterTest(); 1015 global $DB; 1016 1017 $filecontent = 'example content'; 1018 $contenthash = \file_storage::hash_from_string($filecontent); 1019 $filedircontent = [ 1020 '0f' => [ 1021 'f3' => [ 1022 $contenthash => $filecontent, 1023 ], 1024 ], 1025 ]; 1026 $vfileroot = $this->setup_vfile_root($filedircontent); 1027 1028 $DB = $this->getMockBuilder(\moodle_database::class) 1029 ->onlyMethods(['record_exists']) 1030 ->getMockForAbstractClass(); 1031 1032 $DB->method('record_exists')->willReturn(true); 1033 1034 $fs = new file_system_filedir(); 1035 $result = $fs->remove_file($contenthash); 1036 $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash)); 1037 $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash)); 1038 } 1039 1040 /** 1041 * Ensure that remove_file removes the file when it is no 1042 * longer in use. 1043 * 1044 * @covers ::remove_file 1045 */ 1046 public function test_remove_file_expired() { 1047 $this->resetAfterTest(); 1048 global $DB; 1049 1050 $filecontent = 'example content'; 1051 $contenthash = \file_storage::hash_from_string($filecontent); 1052 $filedircontent = [ 1053 '0f' => [ 1054 'f3' => [ 1055 $contenthash => $filecontent, 1056 ], 1057 ], 1058 ]; 1059 $vfileroot = $this->setup_vfile_root($filedircontent); 1060 1061 $DB = $this->getMockBuilder(\moodle_database::class) 1062 ->onlyMethods(['record_exists']) 1063 ->getMockForAbstractClass(); 1064 1065 $DB->method('record_exists')->willReturn(false); 1066 1067 $fs = new file_system_filedir(); 1068 $result = $fs->remove_file($contenthash); 1069 $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash)); 1070 $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash)); 1071 } 1072 1073 /** 1074 * Test purging the cache. 1075 * 1076 * @covers ::empty_trash 1077 */ 1078 public function test_empty_trash() { 1079 $this->resetAfterTest(); 1080 1081 $filecontent = 'example content'; 1082 $contenthash = \file_storage::hash_from_string($filecontent); 1083 1084 $filedircontent = $trashdircontent = [ 1085 '0f' => [ 1086 'f3' => [ 1087 $contenthash => $filecontent, 1088 ], 1089 ], 1090 ]; 1091 $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent); 1092 1093 $fs = new file_system_filedir(); 1094 $method = new \ReflectionMethod(file_system_filedir::class, 'empty_trash'); 1095 $method->setAccessible(true); 1096 $result = $method->invoke($fs); 1097 1098 $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash)); 1099 $this->assertFalse($vfileroot->hasChild('trashdir')); 1100 $this->assertFalse($vfileroot->hasChild('trashdir/0f')); 1101 $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3')); 1102 $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash)); 1103 } 1104 1105 /** 1106 * Data Provider for contenthash to contendir conversion. 1107 * 1108 * @return array 1109 */ 1110 public function contenthash_dataprovider() { 1111 return array( 1112 array( 1113 'contenthash' => 'eee4943847a35a4b6942c6f96daafde06bcfdfab', 1114 'contentdir' => 'ee/e4', 1115 ), 1116 array( 1117 'contenthash' => 'aef05a62ae81ca0005d2569447779af062b7cda0', 1118 'contentdir' => 'ae/f0', 1119 ), 1120 ); 1121 } 1122 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body