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