Merge branch 'MDL-64506' of git://github.com/Chocolate-lightning/moodle
[moodle.git] / lib / filestorage / tests / file_system_filedir_test.php
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/>.
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  */
26 defined('MOODLE_INTERNAL') || die();
28 global $CFG;
29 require_once($CFG->libdir . '/filestorage/file_system.php');
30 require_once($CFG->libdir . '/filestorage/file_system_filedir.php');
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 {
43     /**
44      * Shared test setUp.
45      */
46     public function setUp() {
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     }
52     /**
53      * Shared teset tearDown.
54      */
55     public function tearDown() {
56         // Reset the file storage so that subsequent tests will use the standard file storage.
57         get_file_storage(true);
58     }
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();
72         $content = [];
73         if ($filedir !== null) {
74             $content['filedir'] = $filedir;
75         }
77         if ($trashdir !== null) {
78             $content['trashdir'] = $trashdir;
79         }
81         if ($sourcedir !== null) {
82             $content['sourcedir'] = $sourcedir;
83         }
85         $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, $content);
87         $CFG->filedir = \org\bovigo\vfs\vfsStream::url('root/filedir');
88         $CFG->trashdir = \org\bovigo\vfs\vfsStream::url('root/trashdir');
90         return $vfileroot;
91     }
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         }
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();
120         return $file;
121     }
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();
135         return $fs;
136     }
138     /**
139      * Ensure that an appropriate error is shown when the filedir directory
140      * is not writable.
141      *
142      * @covers ::__construct
143      * @covers ::<!public>
144      */
145     public function test_readonly_filesystem_filedir() {
146         $this->resetAfterTest();
148         // Setup the filedir but remove permissions.
149         $vfileroot = $this->setup_vfile_root(null);
151         // Make the target path readonly.
152         $vfileroot->chmod(0444)
153             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
155         // This should generate an exception.
156         $this->expectException('file_exception');
157         $this->expectExceptionMessageRegExp(
158             '/Can not create local file pool directories, please verify permissions in dataroot./');
160         new file_system_filedir();
161     }
163     /**
164      * Ensure that an appropriate error is shown when the trash directory
165      * is not writable.
166      *
167      * @covers ::__construct
168      * @covers ::<!public>
169      */
170     public function test_readonly_filesystem_trashdir() {
171         $this->resetAfterTest();
173         // Setup the trashdir but remove permissions.
174         $vfileroot = $this->setup_vfile_root([], null);
176         // Make the target path readonly.
177         $vfileroot->chmod(0444)
178             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
180         // This should generate an exception.
181         $this->expectException('file_exception');
182         $this->expectExceptionMessageRegExp(
183             '/Can not create local file pool directories, please verify permissions in dataroot./');
185         new file_system_filedir();
186     }
188     /**
189      * Test that the standard Moodle warning message is put into the filedir.
190      *
191      * @covers ::__construct
192      * @covers ::<!public>
193      */
194     public function test_warnings_put_in_place() {
195         $this->resetAfterTest();
197         $vfileroot = $this->setup_vfile_root(null);
199         new file_system_filedir();
200         $this->assertTrue($vfileroot->hasChild('filedir/warning.txt'));
201         $this->assertEquals(
202             'This directory contains the content of uploaded files and is controlled by Moodle code. ' .
203                 'Do not manually move, change or rename any of the files and subdirectories here.',
204             $vfileroot->getChild('filedir/warning.txt')->getContent()
205         );
206     }
208     /**
209      * Ensure that the default implementation of get_remote_path_from_hash
210      * simply calls get_local_path_from_hash.
211      *
212      * @covers ::get_remote_path_from_hash
213      * @covers ::<!public>
214      */
215     public function test_get_remote_path_from_hash() {
216         $filecontent = 'example content';
217         $contenthash = file_storage::hash_from_string($filecontent);
218         $expectedresult = (object) [];
220         $fs = $this->get_testable_mock([
221             'get_local_path_from_hash',
222         ]);
224         $fs->expects($this->once())
225             ->method('get_local_path_from_hash')
226             ->with($this->equalTo($contenthash), $this->equalTo(false))
227             ->willReturn($expectedresult);
229         $method = new ReflectionMethod(file_system_filedir::class, 'get_remote_path_from_hash');
230         $method->setAccessible(true);
231         $result = $method->invokeArgs($fs, [$contenthash]);
233         $this->assertEquals($expectedresult, $result);
234     }
236     /**
237      * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
238      * a failed recovery.
239      *
240      * @covers ::get_local_path_from_storedfile
241      * @covers ::<!public>
242      */
243     public function test_get_local_path_from_storedfile_with_recovery() {
244         $filecontent = 'example content';
245         $file = $this->get_stored_file($filecontent);
246         $fs = $this->get_testable_mock([
247             'get_local_path_from_hash',
248             'recover_file',
249         ]);
250         $filepath = '/path/to/nonexistent/file';
252         $fs->method('get_local_path_from_hash')
253             ->willReturn($filepath);
255         $fs->expects($this->once())
256             ->method('recover_file')
257             ->with($this->equalTo($file));
259         $file = $this->get_stored_file('example content');
260         $method = new ReflectionMethod(file_system_filedir::class, 'get_local_path_from_storedfile');
261         $method->setAccessible(true);
262         $result = $method->invokeArgs($fs, array($file, true));
264         $this->assertEquals($filepath, $result);
265     }
267     /**
268      * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
269      * a failed recovery.
270      *
271      * @covers ::get_local_path_from_storedfile
272      * @covers ::<!public>
273      */
274     public function test_get_local_path_from_storedfile_without_recovery() {
275         $filecontent = 'example content';
276         $file = $this->get_stored_file($filecontent);
277         $fs = $this->get_testable_mock([
278             'get_local_path_from_hash',
279             'recover_file',
280         ]);
281         $filepath = '/path/to/nonexistent/file';
283         $fs->method('get_local_path_from_hash')
284             ->willReturn($filepath);
286         $fs->expects($this->never())
287             ->method('recover_file');
289         $file = $this->get_stored_file('example content');
290         $method = new ReflectionMethod(file_system_filedir::class, 'get_local_path_from_storedfile');
291         $method->setAccessible(true);
292         $result = $method->invokeArgs($fs, array($file, false));
294         $this->assertEquals($filepath, $result);
295     }
297     /**
298      * Test that the correct path is generated for the supplied content
299      * hashes.
300      *
301      * @dataProvider contenthash_dataprovider
302      * @param   string  $hash contenthash to test
303      * @param   string  $hashdir Expected format of content directory
304      *
305      * @covers ::get_fulldir_from_hash
306      * @covers ::<!public>
307      */
308     public function test_get_fulldir_from_hash($hash, $hashdir) {
309         global $CFG;
311         $fs = new file_system_filedir();
312         $method = new ReflectionMethod(file_system_filedir::class, 'get_fulldir_from_hash');
313         $method->setAccessible(true);
314         $result = $method->invokeArgs($fs, array($hash));
316         $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir);
317         $this->assertEquals($expectedpath, $result);
318     }
320     /**
321      * Test that the correct path is generated for the supplied content
322      * hashes when used with a stored_file.
323      *
324      * @dataProvider contenthash_dataprovider
325      * @param   string  $hash contenthash to test
326      * @param   string  $hashdir Expected format of content directory
327      *
328      * @covers ::get_fulldir_from_storedfile
329      * @covers ::<!public>
330      */
331     public function test_get_fulldir_from_storedfile($hash, $hashdir) {
332         global $CFG;
334         $file = $this->getMockBuilder('stored_file')
335             ->disableOriginalConstructor()
336             ->setMethods([
337                 'sync_external_file',
338                 'get_contenthash',
339             ])
340             ->getMock();
342         $file->method('get_contenthash')->willReturn($hash);
344         $fs = new file_system_filedir();
345         $method = new ReflectionMethod('file_system_filedir', 'get_fulldir_from_storedfile');
346         $method->setAccessible(true);
347         $result = $method->invokeArgs($fs, array($file));
349         $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir);
350         $this->assertEquals($expectedpath, $result);
351     }
353     /**
354      * Test that the correct content directory is generated for the supplied
355      * content hashes.
356      *
357      * @dataProvider contenthash_dataprovider
358      * @param   string  $hash contenthash to test
359      * @param   string  $hashdir Expected format of content directory
360      *
361      * @covers ::get_contentdir_from_hash
362      * @covers ::<!public>
363      */
364     public function test_get_contentdir_from_hash($hash, $hashdir) {
365         $method = new ReflectionMethod(file_system_filedir::class, 'get_contentdir_from_hash');
366         $method->setAccessible(true);
368         $fs = new file_system_filedir();
369         $result = $method->invokeArgs($fs, array($hash));
371         $this->assertEquals($hashdir, $result);
372     }
374     /**
375      * Test that the correct content path is generated for the supplied
376      * content hashes.
377      *
378      * @dataProvider contenthash_dataprovider
379      * @param   string  $hash contenthash to test
380      * @param   string  $hashdir Expected format of content directory
381      *
382      * @covers ::get_contentpath_from_hash
383      * @covers ::<!public>
384      */
385     public function test_get_contentpath_from_hash($hash, $hashdir) {
386         $method = new ReflectionMethod(file_system_filedir::class, 'get_contentpath_from_hash');
387         $method->setAccessible(true);
389         $fs = new file_system_filedir();
390         $result = $method->invokeArgs($fs, array($hash));
392         $expectedpath = sprintf('%s/%s', $hashdir, $hash);
393         $this->assertEquals($expectedpath, $result);
394     }
396     /**
397      * Test that the correct trash path is generated for the supplied
398      * content hashes.
399      *
400      * @dataProvider contenthash_dataprovider
401      * @param   string  $hash contenthash to test
402      * @param   string  $hashdir Expected format of content directory
403      *
404      * @covers ::get_trash_fullpath_from_hash
405      * @covers ::<!public>
406      */
407     public function test_get_trash_fullpath_from_hash($hash, $hashdir) {
408         global $CFG;
410         $fs = new file_system_filedir();
411         $method = new ReflectionMethod(file_system_filedir::class, 'get_trash_fullpath_from_hash');
412         $method->setAccessible(true);
413         $result = $method->invokeArgs($fs, array($hash));
415         $expectedpath = sprintf('%s/trashdir/%s/%s', $CFG->dataroot, $hashdir, $hash);
416         $this->assertEquals($expectedpath, $result);
417     }
419     /**
420      * Test that the correct trash directory is generated for the supplied
421      * content hashes.
422      *
423      * @dataProvider contenthash_dataprovider
424      * @param   string  $hash contenthash to test
425      * @param   string  $hashdir Expected format of content directory
426      *
427      * @covers ::get_trash_fulldir_from_hash
428      * @covers ::<!public>
429      */
430     public function test_get_trash_fulldir_from_hash($hash, $hashdir) {
431         global $CFG;
433         $fs = new file_system_filedir();
434         $method = new ReflectionMethod(file_system_filedir::class, 'get_trash_fulldir_from_hash');
435         $method->setAccessible(true);
436         $result = $method->invokeArgs($fs, array($hash));
438         $expectedpath = sprintf('%s/trashdir/%s', $CFG->dataroot, $hashdir);
439         $this->assertEquals($expectedpath, $result);
440     }
442     /**
443      * Ensure that copying a file to a target from a stored_file works as anticipated.
444      *
445      * @covers ::copy_content_from_storedfile
446      * @covers ::<!public>
447      */
448     public function test_copy_content_from_storedfile() {
449         $this->resetAfterTest();
450         global $CFG;
452         $filecontent = 'example content';
453         $contenthash = file_storage::hash_from_string($filecontent);
454         $filedircontent = [
455             $contenthash => $filecontent,
456         ];
457         $vfileroot = $this->setup_vfile_root($filedircontent, [], []);
459         $fs = $this->getMockBuilder(file_system_filedir::class)
460             ->disableOriginalConstructor()
461             ->setMethods([
462                 'get_local_path_from_storedfile',
463             ])
464             ->getMock();
466         $file = $this->getMockBuilder(stored_file::class)
467             ->disableOriginalConstructor()
468             ->getMock();
470         $sourcefile = \org\bovigo\vfs\vfsStream::url('root/filedir/' . $contenthash);
471         $fs->method('get_local_path_from_storedfile')->willReturn($sourcefile);
473         $targetfile = \org\bovigo\vfs\vfsStream::url('root/targetfile');
474         $CFG->preventfilelocking = true;
475         $result = $fs->copy_content_from_storedfile($file, $targetfile);
477         $this->assertTrue($result);
478         $this->assertEquals($filecontent, $vfileroot->getChild('targetfile')->getContent());
479     }
481     /**
482      * Ensure that content recovery works.
483      *
484      * @covers ::recover_file
485      * @covers ::<!public>
486      */
487     public function test_recover_file() {
488         $this->resetAfterTest();
490         // Setup the filedir.
491         // This contains a virtual file which has a cache mismatch.
492         $filecontent = 'example content';
493         $contenthash = file_storage::hash_from_string($filecontent);
495         $trashdircontent = [
496             '0f' => [
497                 'f3' => [
498                     $contenthash => $filecontent,
499                 ],
500             ],
501         ];
503         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
505         $file = new stored_file(get_file_storage(), (object) [
506             'contenthash' => $contenthash,
507             'filesize' => strlen($filecontent),
508         ]);
510         $fs = new file_system_filedir();
511         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
512         $method->setAccessible(true);
513         $result = $method->invokeArgs($fs, array($file));
515         // Test the output.
516         $this->assertTrue($result);
518         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
520     }
522     /**
523      * Ensure that content recovery works.
524      *
525      * @covers ::recover_file
526      * @covers ::<!public>
527      */
528     public function test_recover_file_already_present() {
529         $this->resetAfterTest();
531         // Setup the filedir.
532         // This contains a virtual file which has a cache mismatch.
533         $filecontent = 'example content';
534         $contenthash = file_storage::hash_from_string($filecontent);
536         $filedircontent = $trashdircontent = [
537             '0f' => [
538                 'f3' => [
539                     $contenthash => $filecontent,
540                 ],
541             ],
542         ];
544         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
546         $file = new stored_file(get_file_storage(), (object) [
547             'contenthash' => $contenthash,
548             'filesize' => strlen($filecontent),
549         ]);
551         $fs = new file_system_filedir();
552         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
553         $method->setAccessible(true);
554         $result = $method->invokeArgs($fs, array($file));
556         // Test the output.
557         $this->assertTrue($result);
559         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
560     }
562     /**
563      * Ensure that content recovery works.
564      *
565      * @covers ::recover_file
566      * @covers ::<!public>
567      */
568     public function test_recover_file_size_mismatch() {
569         $this->resetAfterTest();
571         // Setup the filedir.
572         // This contains a virtual file which has a cache mismatch.
573         $filecontent = 'example content';
574         $contenthash = file_storage::hash_from_string($filecontent);
576         $trashdircontent = [
577             '0f' => [
578                 'f3' => [
579                     $contenthash => $filecontent,
580                 ],
581             ],
582         ];
583         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
585         $file = new stored_file(get_file_storage(), (object) [
586             'contenthash' => $contenthash,
587             'filesize' => strlen($filecontent) + 1,
588         ]);
590         $fs = new file_system_filedir();
591         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
592         $method->setAccessible(true);
593         $result = $method->invokeArgs($fs, array($file));
595         // Test the output.
596         $this->assertFalse($result);
597         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
598     }
600     /**
601      * Ensure that content recovery works.
602      *
603      * @covers ::recover_file
604      * @covers ::<!public>
605      */
606     public function test_recover_file_has_mismatch() {
607         $this->resetAfterTest();
609         // Setup the filedir.
610         // This contains a virtual file which has a cache mismatch.
611         $filecontent = 'example content';
612         $contenthash = file_storage::hash_from_string($filecontent);
614         $trashdircontent = [
615             '0f' => [
616                 'f3' => [
617                     $contenthash => $filecontent,
618                 ],
619             ],
620         ];
621         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
623         $file = new stored_file(get_file_storage(), (object) [
624             'contenthash' => $contenthash . " different",
625             'filesize' => strlen($filecontent),
626         ]);
628         $fs = new file_system_filedir();
629         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
630         $method->setAccessible(true);
631         $result = $method->invokeArgs($fs, array($file));
633         // Test the output.
634         $this->assertFalse($result);
635         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
636     }
638     /**
639      * Ensure that content recovery works when the content file is in the
640      * alt trash directory.
641      *
642      * @covers ::recover_file
643      * @covers ::<!public>
644      */
645     public function test_recover_file_alttrash() {
646         $this->resetAfterTest();
648         // Setup the filedir.
649         // This contains a virtual file which has a cache mismatch.
650         $filecontent = 'example content';
651         $contenthash = file_storage::hash_from_string($filecontent);
653         $trashdircontent = [
654             $contenthash => $filecontent,
655         ];
656         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
658         $file = new stored_file(get_file_storage(), (object) [
659             'contenthash' => $contenthash,
660             'filesize' => strlen($filecontent),
661         ]);
663         $fs = new file_system_filedir();
664         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
665         $method->setAccessible(true);
666         $result = $method->invokeArgs($fs, array($file));
668         // Test the output.
669         $this->assertTrue($result);
671         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
672     }
674     /**
675      * Test that an appropriate error message is generated when adding a
676      * file to the pool when the pool directory structure is not writable.
677      *
678      * @covers ::recover_file
679      * @covers ::<!public>
680      */
681     public function test_recover_file_contentdir_readonly() {
682         $this->resetAfterTest();
684         $filecontent = 'example content';
685         $contenthash = file_storage::hash_from_string($filecontent);
686         $filedircontent = [
687             '0f' => [],
688         ];
689         $trashdircontent = [
690             $contenthash => $filecontent,
691         ];
692         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
694         // Make the target path readonly.
695         $vfileroot->getChild('filedir/0f')
696             ->chmod(0444)
697             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
699         $file = new stored_file(get_file_storage(), (object) [
700             'contenthash' => $contenthash,
701             'filesize' => strlen($filecontent),
702         ]);
704         $fs = new file_system_filedir();
705         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
706         $method->setAccessible(true);
707         $result = $method->invokeArgs($fs, array($file));
709         // Test the output.
710         $this->assertFalse($result);
711     }
713     /**
714      * Test adding a file to the pool.
715      *
716      * @covers ::add_file_from_path
717      * @covers ::<!public>
718      */
719     public function test_add_file_from_path() {
720         $this->resetAfterTest();
721         global $CFG;
723         // Setup the filedir.
724         // This contains a virtual file which has a cache mismatch.
725         $filecontent = 'example content';
726         $contenthash = file_storage::hash_from_string($filecontent);
727         $sourcedircontent = [
728             'file' => $filecontent,
729         ];
731         $vfileroot = $this->setup_vfile_root([], [], $sourcedircontent);
733         // Note, the vfs file system does not support locks - prevent file locking here.
734         $CFG->preventfilelocking = true;
736         // Attempt to add the file to the file pool.
737         $fs = new file_system_filedir();
738         $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
739         $result = $fs->add_file_from_path($sourcefile);
741         // Test the output.
742         $this->assertEquals($contenthash, $result[0]);
743         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
744         $this->assertTrue($result[2]);
746         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
747     }
749     /**
750      * Test that an appropriate error message is generated when adding an
751      * unavailable file to the pool is attempted.
752      *
753      * @covers ::add_file_from_path
754      * @covers ::<!public>
755      */
756     public function test_add_file_from_path_file_unavailable() {
757         $this->resetAfterTest();
759         // Setup the filedir.
760         $vfileroot = $this->setup_vfile_root();
762         $this->expectException('file_exception');
763         $this->expectExceptionMessageRegExp(
764             '/Cannot read file\. Either the file does not exist or there is a permission problem\./');
766         $fs = new file_system_filedir();
767         $fs->add_file_from_path(\org\bovigo\vfs\vfsStream::url('filedir/file'));
768     }
770     /**
771      * Test that an appropriate error message is generated when specifying
772      * the wrong contenthash when adding a file to the pool.
773      *
774      * @covers ::add_file_from_path
775      * @covers ::<!public>
776      */
777     public function test_add_file_from_path_mismatched_hash() {
778         $this->resetAfterTest();
780         $filecontent = 'example content';
781         $contenthash = file_storage::hash_from_string($filecontent);
782         $sourcedir = [
783             'file' => $filecontent,
784         ];
785         $vfileroot = $this->setup_vfile_root([], [], $sourcedir);
787         $fs = new file_system_filedir();
788         $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
789         $fs->add_file_from_path($filepath, 'eee4943847a35a4b6942c6f96daafde06bcfdfab');
790         $this->assertDebuggingCalled("Invalid contenthash submitted for file $filepath");
791     }
793     /**
794      * Test that an appropriate error message is generated when an existing
795      * file in the pool has the wrong contenthash
796      *
797      * @covers ::add_file_from_path
798      * @covers ::<!public>
799      */
800     public function test_add_file_from_path_existing_content_invalid() {
801         $this->resetAfterTest();
803         $filecontent = 'example content';
804         $contenthash = file_storage::hash_from_string($filecontent);
805         $filedircontent = [
806             '0f' => [
807                 'f3' => [
808                     // This contains a virtual file which has a cache mismatch.
809                     '0ff30941ca5acd879fd809e8c937d9f9e6dd1615' => 'different example content',
810                 ],
811             ],
812         ];
813         $sourcedir = [
814             'file' => $filecontent,
815         ];
816         $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir);
818         // Check that we hit the jackpot.
819         $fs = new file_system_filedir();
820         $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
821         $result = $fs->add_file_from_path($filepath);
823         // We provided a bad hash. Check that the file was replaced.
824         $this->assertDebuggingCalled("Replacing invalid content file $contenthash");
826         // Test the output.
827         $this->assertEquals($contenthash, $result[0]);
828         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
829         $this->assertFalse($result[2]);
831         // Fetch the new file structure.
832         $structure = \org\bovigo\vfs\vfsStream::inspect(
833             new \org\bovigo\vfs\visitor\vfsStreamStructureVisitor()
834         )->getStructure();
836         $this->assertEquals($filecontent, $structure['root']['filedir']['0f']['f3'][$contenthash]);
837     }
839     /**
840      * Test that an appropriate error message is generated when adding a
841      * file to the pool when the pool directory structure is not writable.
842      *
843      * @covers ::add_file_from_path
844      * @covers ::<!public>
845      */
846     public function test_add_file_from_path_existing_cannot_write_hashpath() {
847         $this->resetAfterTest();
849         $filecontent = 'example content';
850         $contenthash = file_storage::hash_from_string($filecontent);
851         $filedircontent = [
852             '0f' => [],
853         ];
854         $sourcedir = [
855             'file' => $filecontent,
856         ];
857         $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir);
859         // Make the target path readonly.
860         $vfileroot->getChild('filedir/0f')
861             ->chmod(0444)
862             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
864         $this->expectException('file_exception');
865         $this->expectExceptionMessageRegExp(
866             "/Can not create local file pool directories, please verify permissions in dataroot./");
868         // Attempt to add the file to the file pool.
869         $fs = new file_system_filedir();
870         $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
871         $fs->add_file_from_path($sourcefile);
872     }
874     /**
875      * Test adding a string to the pool.
876      *
877      * @covers ::add_file_from_string
878      * @covers ::<!public>
879      */
880     public function test_add_file_from_string() {
881         $this->resetAfterTest();
882         global $CFG;
884         $filecontent = 'example content';
885         $contenthash = file_storage::hash_from_string($filecontent);
886         $vfileroot = $this->setup_vfile_root();
888         // Note, the vfs file system does not support locks - prevent file locking here.
889         $CFG->preventfilelocking = true;
891         // Attempt to add the file to the file pool.
892         $fs = new file_system_filedir();
893         $result = $fs->add_file_from_string($filecontent);
895         // Test the output.
896         $this->assertEquals($contenthash, $result[0]);
897         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
898         $this->assertTrue($result[2]);
899     }
901     /**
902      * Test that an appropriate error message is generated when adding a
903      * string to the pool when the pool directory structure is not writable.
904      *
905      * @covers ::add_file_from_string
906      * @covers ::<!public>
907      */
908     public function test_add_file_from_string_existing_cannot_write_hashpath() {
909         $this->resetAfterTest();
911         $filecontent = 'example content';
912         $contenthash = file_storage::hash_from_string($filecontent);
914         $filedircontent = [
915             '0f' => [],
916         ];
917         $vfileroot = $this->setup_vfile_root($filedircontent);
919         // Make the target path readonly.
920         $vfileroot->getChild('filedir/0f')
921             ->chmod(0444)
922             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
924         $this->expectException('file_exception');
925         $this->expectExceptionMessageRegExp(
926             "/Can not create local file pool directories, please verify permissions in dataroot./");
928         // Attempt to add the file to the file pool.
929         $fs = new file_system_filedir();
930         $fs->add_file_from_string($filecontent);
931     }
933     /**
934      * Test adding a string to the pool when an item with the same
935      * contenthash is already present.
936      *
937      * @covers ::add_file_from_string
938      * @covers ::<!public>
939      */
940     public function test_add_file_from_string_existing_matches() {
941         $this->resetAfterTest();
942         global $CFG;
944         $filecontent = 'example content';
945         $contenthash = file_storage::hash_from_string($filecontent);
946         $filedircontent = [
947             '0f' => [
948                 'f3' => [
949                     $contenthash => $filecontent,
950                 ],
951             ],
952         ];
954         $vfileroot = $this->setup_vfile_root($filedircontent);
956         // Note, the vfs file system does not support locks - prevent file locking here.
957         $CFG->preventfilelocking = true;
959         // Attempt to add the file to the file pool.
960         $fs = new file_system_filedir();
961         $result = $fs->add_file_from_string($filecontent);
963         // Test the output.
964         $this->assertEquals($contenthash, $result[0]);
965         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
966         $this->assertFalse($result[2]);
967     }
969     /**
970      * Test the cleanup of deleted files when there are no files to delete.
971      *
972      * @covers ::remove_file
973      * @covers ::<!public>
974      */
975     public function test_remove_file_missing() {
976         $this->resetAfterTest();
978         $filecontent = 'example content';
979         $contenthash = file_storage::hash_from_string($filecontent);
980         $vfileroot = $this->setup_vfile_root();
982         $fs = new file_system_filedir();
983         $fs->remove_file($contenthash);
985         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
986         // No file to move to trash, so the trash path will also be empty.
987         $this->assertFalse($vfileroot->hasChild('trashdir/0f'));
988         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3'));
989         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
990     }
992     /**
993      * Test the cleanup of deleted files when a file already exists in the
994      * trash for that path.
995      *
996      * @covers ::remove_file
997      * @covers ::<!public>
998      */
999     public function test_remove_file_existing_trash() {
1000         $this->resetAfterTest();
1002         $filecontent = 'example content';
1003         $contenthash = file_storage::hash_from_string($filecontent);
1005         $filedircontent = $trashdircontent = [
1006             '0f' => [
1007                 'f3' => [
1008                     $contenthash => $filecontent,
1009                 ],
1010             ],
1011         ];
1012         $trashdircontent['0f']['f3'][$contenthash] .= 'different';
1013         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
1015         $fs = new file_system_filedir();
1016         $fs->remove_file($contenthash);
1018         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1019         $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1020         $this->assertNotEquals($filecontent, $vfileroot->getChild('trashdir/0f/f3/' . $contenthash)->getContent());
1021     }
1023     /**
1024      * Ensure that remove_file does nothing with an empty file.
1025      *
1026      * @covers ::remove_file
1027      * @covers ::<!public>
1028      */
1029     public function test_remove_file_empty() {
1030         $this->resetAfterTest();
1031         global $DB;
1033         $DB = $this->getMockBuilder(\moodle_database::class)
1034             ->setMethods(['record_exists'])
1035             ->getMockForAbstractClass();
1037         $DB->expects($this->never())
1038             ->method('record_exists');
1040         $fs = new file_system_filedir();
1042         $result = $fs->remove_file(file_storage::hash_from_string(''));
1043         $this->assertNull($result);
1044     }
1046     /**
1047      * Ensure that remove_file does nothing when a file is still
1048      * in use.
1049      *
1050      * @covers ::remove_file
1051      * @covers ::<!public>
1052      */
1053     public function test_remove_file_in_use() {
1054         $this->resetAfterTest();
1055         global $DB;
1057         $filecontent = 'example content';
1058         $contenthash = file_storage::hash_from_string($filecontent);
1059         $filedircontent = [
1060             '0f' => [
1061                 'f3' => [
1062                     $contenthash => $filecontent,
1063                 ],
1064             ],
1065         ];
1066         $vfileroot = $this->setup_vfile_root($filedircontent);
1068         $DB = $this->getMockBuilder(\moodle_database::class)
1069             ->setMethods(['record_exists'])
1070             ->getMockForAbstractClass();
1072         $DB->method('record_exists')->willReturn(true);
1074         $fs = new file_system_filedir();
1075         $result = $fs->remove_file($contenthash);
1076         $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1077         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1078     }
1080     /**
1081      * Ensure that remove_file removes the file when it is no
1082      * longer in use.
1083      *
1084      * @covers ::remove_file
1085      * @covers ::<!public>
1086      */
1087     public function test_remove_file_expired() {
1088         $this->resetAfterTest();
1089         global $DB;
1091         $filecontent = 'example content';
1092         $contenthash = file_storage::hash_from_string($filecontent);
1093         $filedircontent = [
1094             '0f' => [
1095                 'f3' => [
1096                     $contenthash => $filecontent,
1097                 ],
1098             ],
1099         ];
1100         $vfileroot = $this->setup_vfile_root($filedircontent);
1102         $DB = $this->getMockBuilder(\moodle_database::class)
1103             ->setMethods(['record_exists'])
1104             ->getMockForAbstractClass();
1106         $DB->method('record_exists')->willReturn(false);
1108         $fs = new file_system_filedir();
1109         $result = $fs->remove_file($contenthash);
1110         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1111         $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1112     }
1114     /**
1115      * Test purging the cache.
1116      *
1117      * @covers ::empty_trash
1118      * @covers ::<!public>
1119      */
1120     public function test_empty_trash() {
1121         $this->resetAfterTest();
1123         $filecontent = 'example content';
1124         $contenthash = file_storage::hash_from_string($filecontent);
1126         $filedircontent = $trashdircontent = [
1127             '0f' => [
1128                 'f3' => [
1129                     $contenthash => $filecontent,
1130                 ],
1131             ],
1132         ];
1133         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
1135         $fs = new file_system_filedir();
1136         $method = new ReflectionMethod(file_system_filedir::class, 'empty_trash');
1137         $method->setAccessible(true);
1138         $result = $method->invoke($fs);
1140         $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1141         $this->assertFalse($vfileroot->hasChild('trashdir'));
1142         $this->assertFalse($vfileroot->hasChild('trashdir/0f'));
1143         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3'));
1144         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1145     }
1147     /**
1148      * Data Provider for contenthash to contendir conversion.
1149      *
1150      * @return  array
1151      */
1152     public function contenthash_dataprovider() {
1153         return array(
1154             array(
1155                 'contenthash'   => 'eee4943847a35a4b6942c6f96daafde06bcfdfab',
1156                 'contentdir'    => 'ee/e4',
1157             ),
1158             array(
1159                 'contenthash'   => 'aef05a62ae81ca0005d2569447779af062b7cda0',
1160                 'contentdir'    => 'ae/f0',
1161             ),
1162         );
1163     }