MDL-64506 templates: BS2 pull-x -> BS4 float-x
[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  */
40 class core_files_file_system_filedir_testcase extends advanced_testcase {
42     /**
43      * Shared test setUp.
44      */
45     public function setUp() {
46         // Reset the file storage so that subsequent fetches to get_file_storage are called after
47         // configuration is prepared.
48         get_file_storage(true);
49     }
51     /**
52      * Shared teset tearDown.
53      */
54     public function tearDown() {
55         // Reset the file storage so that subsequent tests will use the standard file storage.
56         get_file_storage(true);
57     }
59     /**
60      * Helper function to help setup and configure the virtual file system stream.
61      *
62      * @param   array $filedir Directory structure and content of the filedir
63      * @param   array $trashdir Directory structure and content of the sourcedir
64      * @param   array $sourcedir Directory structure and content of a directory used for source files for tests
65      * @return  \org\bovigo\vfs\vfsStream
66      */
67     protected function setup_vfile_root($filedir = [], $trashdir = [], $sourcedir = null) {
68         global $CFG;
69         $this->resetAfterTest();
71         $content = [];
72         if ($filedir !== null) {
73             $content['filedir'] = $filedir;
74         }
76         if ($trashdir !== null) {
77             $content['trashdir'] = $trashdir;
78         }
80         if ($sourcedir !== null) {
81             $content['sourcedir'] = $sourcedir;
82         }
84         $vfileroot = \org\bovigo\vfs\vfsStream::setup('root', null, $content);
86         $CFG->filedir = \org\bovigo\vfs\vfsStream::url('root/filedir');
87         $CFG->trashdir = \org\bovigo\vfs\vfsStream::url('root/trashdir');
89         return $vfileroot;
90     }
92     /**
93      * Helper to create a stored file objectw with the given supplied content.
94      *
95      * @param   string  $filecontent The content of the mocked file
96      * @param   string  $filename The file name to use in the stored_file
97      * @param   array   $mockedmethods A list of methods you intend to override
98      *                  If no methods are specified, only abstract functions are mocked.
99      * @return stored_file
100      */
101     protected function get_stored_file($filecontent, $filename = null, $mockedmethods = null) {
102         $contenthash = file_storage::hash_from_string($filecontent);
103         if (empty($filename)) {
104             $filename = $contenthash;
105         }
107         $file = $this->getMockBuilder(stored_file::class)
108             ->setMethods($mockedmethods)
109             ->setConstructorArgs([
110                 get_file_storage(),
111                 (object) [
112                     'contenthash' => $contenthash,
113                     'filesize' => strlen($filecontent),
114                     'filename' => $filename,
115                 ]
116             ])
117             ->getMock();
119         return $file;
120     }
122     /**
123      * Get a testable mock of the file_system_filedir class.
124      *
125      * @param   array   $mockedmethods A list of methods you intend to override
126      *                  If no methods are specified, only abstract functions are mocked.
127      * @return file_system
128      */
129     protected function get_testable_mock($mockedmethods = []) {
130         $fs = $this->getMockBuilder(file_system_filedir::class)
131             ->setMethods($mockedmethods)
132             ->getMock();
134         return $fs;
135     }
137     /**
138      * Ensure that an appropriate error is shown when the filedir directory
139      * is not writable.
140      */
141     public function test_readonly_filesystem_filedir() {
142         $this->resetAfterTest();
144         // Setup the filedir but remove permissions.
145         $vfileroot = $this->setup_vfile_root(null);
147         // Make the target path readonly.
148         $vfileroot->chmod(0444)
149             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
151         // This should generate an exception.
152         $this->expectException('file_exception');
153         $this->expectExceptionMessageRegExp(
154             '/Can not create local file pool directories, please verify permissions in dataroot./');
156         new file_system_filedir();
157     }
159     /**
160      * Ensure that an appropriate error is shown when the trash directory
161      * is not writable.
162      */
163     public function test_readonly_filesystem_trashdir() {
164         $this->resetAfterTest();
166         // Setup the trashdir but remove permissions.
167         $vfileroot = $this->setup_vfile_root([], null);
169         // Make the target path readonly.
170         $vfileroot->chmod(0444)
171             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
173         // This should generate an exception.
174         $this->expectException('file_exception');
175         $this->expectExceptionMessageRegExp(
176             '/Can not create local file pool directories, please verify permissions in dataroot./');
178         new file_system_filedir();
179     }
181     /**
182      * Test that the standard Moodle warning message is put into the filedir.
183      */
184     public function test_warnings_put_in_place() {
185         $this->resetAfterTest();
187         $vfileroot = $this->setup_vfile_root(null);
189         new file_system_filedir();
190         $this->assertTrue($vfileroot->hasChild('filedir/warning.txt'));
191         $this->assertEquals(
192             'This directory contains the content of uploaded files and is controlled by Moodle code. ' .
193                 'Do not manually move, change or rename any of the files and subdirectories here.',
194             $vfileroot->getChild('filedir/warning.txt')->getContent()
195         );
196     }
198     /**
199      * Ensure that the default implementation of get_remote_path_from_hash
200      * simply calls get_local_path_from_hash.
201      */
202     public function test_get_remote_path_from_hash() {
203         $filecontent = 'example content';
204         $contenthash = file_storage::hash_from_string($filecontent);
205         $expectedresult = (object) [];
207         $fs = $this->get_testable_mock([
208             'get_local_path_from_hash',
209         ]);
211         $fs->expects($this->once())
212             ->method('get_local_path_from_hash')
213             ->with($this->equalTo($contenthash), $this->equalTo(false))
214             ->willReturn($expectedresult);
216         $method = new ReflectionMethod(file_system_filedir::class, 'get_remote_path_from_hash');
217         $method->setAccessible(true);
218         $result = $method->invokeArgs($fs, [$contenthash]);
220         $this->assertEquals($expectedresult, $result);
221     }
223     /**
224      * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
225      * a failed recovery.
226      */
227     public function test_get_local_path_from_storedfile_with_recovery() {
228         $filecontent = 'example content';
229         $file = $this->get_stored_file($filecontent);
230         $fs = $this->get_testable_mock([
231             'get_local_path_from_hash',
232             'recover_file',
233         ]);
234         $filepath = '/path/to/nonexistent/file';
236         $fs->method('get_local_path_from_hash')
237             ->willReturn($filepath);
239         $fs->expects($this->once())
240             ->method('recover_file')
241             ->with($this->equalTo($file));
243         $file = $this->get_stored_file('example content');
244         $method = new ReflectionMethod(file_system_filedir::class, 'get_local_path_from_storedfile');
245         $method->setAccessible(true);
246         $result = $method->invokeArgs($fs, array($file, true));
248         $this->assertEquals($filepath, $result);
249     }
251     /**
252      * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
253      * a failed recovery.
254      */
255     public function test_get_local_path_from_storedfile_without_recovery() {
256         $filecontent = 'example content';
257         $file = $this->get_stored_file($filecontent);
258         $fs = $this->get_testable_mock([
259             'get_local_path_from_hash',
260             'recover_file',
261         ]);
262         $filepath = '/path/to/nonexistent/file';
264         $fs->method('get_local_path_from_hash')
265             ->willReturn($filepath);
267         $fs->expects($this->never())
268             ->method('recover_file');
270         $file = $this->get_stored_file('example content');
271         $method = new ReflectionMethod(file_system_filedir::class, 'get_local_path_from_storedfile');
272         $method->setAccessible(true);
273         $result = $method->invokeArgs($fs, array($file, false));
275         $this->assertEquals($filepath, $result);
276     }
278     /**
279      * Test that the correct path is generated for the supplied content
280      * hashes.
281      *
282      * @dataProvider contenthash_dataprovider
283      * @param   string  $hash contenthash to test
284      * @param   string  $hashdir Expected format of content directory
285      */
286     public function test_get_fulldir_from_hash($hash, $hashdir) {
287         global $CFG;
289         $fs = new file_system_filedir();
290         $method = new ReflectionMethod(file_system_filedir::class, 'get_fulldir_from_hash');
291         $method->setAccessible(true);
292         $result = $method->invokeArgs($fs, array($hash));
294         $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir);
295         $this->assertEquals($expectedpath, $result);
296     }
298     /**
299      * Test that the correct path is generated for the supplied content
300      * hashes when used with a stored_file.
301      *
302      * @dataProvider contenthash_dataprovider
303      * @param   string  $hash contenthash to test
304      * @param   string  $hashdir Expected format of content directory
305      */
306     public function test_get_fulldir_from_storedfile($hash, $hashdir) {
307         global $CFG;
309         $file = $this->getMockBuilder('stored_file')
310             ->disableOriginalConstructor()
311             ->setMethods([
312                 'sync_external_file',
313                 'get_contenthash',
314             ])
315             ->getMock();
317         $file->method('get_contenthash')->willReturn($hash);
319         $fs = new file_system_filedir();
320         $method = new ReflectionMethod('file_system_filedir', 'get_fulldir_from_storedfile');
321         $method->setAccessible(true);
322         $result = $method->invokeArgs($fs, array($file));
324         $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir);
325         $this->assertEquals($expectedpath, $result);
326     }
328     /**
329      * Test that the correct content directory is generated for the supplied
330      * content hashes.
331      *
332      * @dataProvider contenthash_dataprovider
333      * @param   string  $hash contenthash to test
334      * @param   string  $hashdir Expected format of content directory
335      */
336     public function test_get_contentdir_from_hash($hash, $hashdir) {
337         $method = new ReflectionMethod(file_system_filedir::class, 'get_contentdir_from_hash');
338         $method->setAccessible(true);
340         $fs = new file_system_filedir();
341         $result = $method->invokeArgs($fs, array($hash));
343         $this->assertEquals($hashdir, $result);
344     }
346     /**
347      * Test that the correct content path is generated for the supplied
348      * content hashes.
349      *
350      * @dataProvider contenthash_dataprovider
351      * @param   string  $hash contenthash to test
352      * @param   string  $hashdir Expected format of content directory
353      */
354     public function test_get_contentpath_from_hash($hash, $hashdir) {
355         $method = new ReflectionMethod(file_system_filedir::class, 'get_contentpath_from_hash');
356         $method->setAccessible(true);
358         $fs = new file_system_filedir();
359         $result = $method->invokeArgs($fs, array($hash));
361         $expectedpath = sprintf('%s/%s', $hashdir, $hash);
362         $this->assertEquals($expectedpath, $result);
363     }
365     /**
366      * Test that the correct trash path is generated for the supplied
367      * content hashes.
368      *
369      * @dataProvider contenthash_dataprovider
370      * @param   string  $hash contenthash to test
371      * @param   string  $hashdir Expected format of content directory
372      */
373     public function test_get_trash_fullpath_from_hash($hash, $hashdir) {
374         global $CFG;
376         $fs = new file_system_filedir();
377         $method = new ReflectionMethod(file_system_filedir::class, 'get_trash_fullpath_from_hash');
378         $method->setAccessible(true);
379         $result = $method->invokeArgs($fs, array($hash));
381         $expectedpath = sprintf('%s/trashdir/%s/%s', $CFG->dataroot, $hashdir, $hash);
382         $this->assertEquals($expectedpath, $result);
383     }
385     /**
386      * Test that the correct trash directory is generated for the supplied
387      * content hashes.
388      *
389      * @dataProvider contenthash_dataprovider
390      * @param   string  $hash contenthash to test
391      * @param   string  $hashdir Expected format of content directory
392      */
393     public function test_get_trash_fulldir_from_hash($hash, $hashdir) {
394         global $CFG;
396         $fs = new file_system_filedir();
397         $method = new ReflectionMethod(file_system_filedir::class, 'get_trash_fulldir_from_hash');
398         $method->setAccessible(true);
399         $result = $method->invokeArgs($fs, array($hash));
401         $expectedpath = sprintf('%s/trashdir/%s', $CFG->dataroot, $hashdir);
402         $this->assertEquals($expectedpath, $result);
403     }
405     /**
406      * Ensure that copying a file to a target from a stored_file works as anticipated.
407      */
408     public function test_copy_content_from_storedfile() {
409         $this->resetAfterTest();
410         global $CFG;
412         $filecontent = 'example content';
413         $contenthash = file_storage::hash_from_string($filecontent);
414         $filedircontent = [
415             $contenthash => $filecontent,
416         ];
417         $vfileroot = $this->setup_vfile_root($filedircontent, [], []);
419         $fs = $this->getMockBuilder(file_system_filedir::class)
420             ->disableOriginalConstructor()
421             ->setMethods([
422                 'get_local_path_from_storedfile',
423             ])
424             ->getMock();
426         $file = $this->getMockBuilder(stored_file::class)
427             ->disableOriginalConstructor()
428             ->getMock();
430         $sourcefile = \org\bovigo\vfs\vfsStream::url('root/filedir/' . $contenthash);
431         $fs->method('get_local_path_from_storedfile')->willReturn($sourcefile);
433         $targetfile = \org\bovigo\vfs\vfsStream::url('root/targetfile');
434         $CFG->preventfilelocking = true;
435         $result = $fs->copy_content_from_storedfile($file, $targetfile);
437         $this->assertTrue($result);
438         $this->assertEquals($filecontent, $vfileroot->getChild('targetfile')->getContent());
439     }
441     /**
442      * Ensure that content recovery works.
443      */
444     public function test_recover_file() {
445         $this->resetAfterTest();
447         // Setup the filedir.
448         // This contains a virtual file which has a cache mismatch.
449         $filecontent = 'example content';
450         $contenthash = file_storage::hash_from_string($filecontent);
452         $trashdircontent = [
453             '0f' => [
454                 'f3' => [
455                     $contenthash => $filecontent,
456                 ],
457             ],
458         ];
460         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
462         $file = new stored_file(get_file_storage(), (object) [
463             'contenthash' => $contenthash,
464             'filesize' => strlen($filecontent),
465         ]);
467         $fs = new file_system_filedir();
468         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
469         $method->setAccessible(true);
470         $result = $method->invokeArgs($fs, array($file));
472         // Test the output.
473         $this->assertTrue($result);
475         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
477     }
479     /**
480      * Ensure that content recovery works.
481      */
482     public function test_recover_file_already_present() {
483         $this->resetAfterTest();
485         // Setup the filedir.
486         // This contains a virtual file which has a cache mismatch.
487         $filecontent = 'example content';
488         $contenthash = file_storage::hash_from_string($filecontent);
490         $filedircontent = $trashdircontent = [
491             '0f' => [
492                 'f3' => [
493                     $contenthash => $filecontent,
494                 ],
495             ],
496         ];
498         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
500         $file = new stored_file(get_file_storage(), (object) [
501             'contenthash' => $contenthash,
502             'filesize' => strlen($filecontent),
503         ]);
505         $fs = new file_system_filedir();
506         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
507         $method->setAccessible(true);
508         $result = $method->invokeArgs($fs, array($file));
510         // Test the output.
511         $this->assertTrue($result);
513         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
514     }
516     /**
517      * Ensure that content recovery works.
518      */
519     public function test_recover_file_size_mismatch() {
520         $this->resetAfterTest();
522         // Setup the filedir.
523         // This contains a virtual file which has a cache mismatch.
524         $filecontent = 'example content';
525         $contenthash = file_storage::hash_from_string($filecontent);
527         $trashdircontent = [
528             '0f' => [
529                 'f3' => [
530                     $contenthash => $filecontent,
531                 ],
532             ],
533         ];
534         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
536         $file = new stored_file(get_file_storage(), (object) [
537             'contenthash' => $contenthash,
538             'filesize' => strlen($filecontent) + 1,
539         ]);
541         $fs = new file_system_filedir();
542         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
543         $method->setAccessible(true);
544         $result = $method->invokeArgs($fs, array($file));
546         // Test the output.
547         $this->assertFalse($result);
548         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
549     }
551     /**
552      * Ensure that content recovery works.
553      */
554     public function test_recover_file_has_mismatch() {
555         $this->resetAfterTest();
557         // Setup the filedir.
558         // This contains a virtual file which has a cache mismatch.
559         $filecontent = 'example content';
560         $contenthash = file_storage::hash_from_string($filecontent);
562         $trashdircontent = [
563             '0f' => [
564                 'f3' => [
565                     $contenthash => $filecontent,
566                 ],
567             ],
568         ];
569         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
571         $file = new stored_file(get_file_storage(), (object) [
572             'contenthash' => $contenthash . " different",
573             'filesize' => strlen($filecontent),
574         ]);
576         $fs = new file_system_filedir();
577         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
578         $method->setAccessible(true);
579         $result = $method->invokeArgs($fs, array($file));
581         // Test the output.
582         $this->assertFalse($result);
583         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
584     }
586     /**
587      * Ensure that content recovery works when the content file is in the
588      * alt trash directory.
589      */
590     public function test_recover_file_alttrash() {
591         $this->resetAfterTest();
593         // Setup the filedir.
594         // This contains a virtual file which has a cache mismatch.
595         $filecontent = 'example content';
596         $contenthash = file_storage::hash_from_string($filecontent);
598         $trashdircontent = [
599             $contenthash => $filecontent,
600         ];
601         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
603         $file = new stored_file(get_file_storage(), (object) [
604             'contenthash' => $contenthash,
605             'filesize' => strlen($filecontent),
606         ]);
608         $fs = new file_system_filedir();
609         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
610         $method->setAccessible(true);
611         $result = $method->invokeArgs($fs, array($file));
613         // Test the output.
614         $this->assertTrue($result);
616         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
617     }
619     /**
620      * Test that an appropriate error message is generated when adding a
621      * file to the pool when the pool directory structure is not writable.
622      */
623     public function test_recover_file_contentdir_readonly() {
624         $this->resetAfterTest();
626         $filecontent = 'example content';
627         $contenthash = file_storage::hash_from_string($filecontent);
628         $filedircontent = [
629             '0f' => [],
630         ];
631         $trashdircontent = [
632             $contenthash => $filecontent,
633         ];
634         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
636         // Make the target path readonly.
637         $vfileroot->getChild('filedir/0f')
638             ->chmod(0444)
639             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
641         $file = new stored_file(get_file_storage(), (object) [
642             'contenthash' => $contenthash,
643             'filesize' => strlen($filecontent),
644         ]);
646         $fs = new file_system_filedir();
647         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
648         $method->setAccessible(true);
649         $result = $method->invokeArgs($fs, array($file));
651         // Test the output.
652         $this->assertFalse($result);
653     }
655     /**
656      * Test adding a file to the pool.
657      */
658     public function test_add_file_from_path() {
659         $this->resetAfterTest();
660         global $CFG;
662         // Setup the filedir.
663         // This contains a virtual file which has a cache mismatch.
664         $filecontent = 'example content';
665         $contenthash = file_storage::hash_from_string($filecontent);
666         $sourcedircontent = [
667             'file' => $filecontent,
668         ];
670         $vfileroot = $this->setup_vfile_root([], [], $sourcedircontent);
672         // Note, the vfs file system does not support locks - prevent file locking here.
673         $CFG->preventfilelocking = true;
675         // Attempt to add the file to the file pool.
676         $fs = new file_system_filedir();
677         $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
678         $result = $fs->add_file_from_path($sourcefile);
680         // Test the output.
681         $this->assertEquals($contenthash, $result[0]);
682         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
683         $this->assertTrue($result[2]);
685         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
686     }
688     /**
689      * Test that an appropriate error message is generated when adding an
690      * unavailable file to the pool is attempted.
691      */
692     public function test_add_file_from_path_file_unavailable() {
693         $this->resetAfterTest();
695         // Setup the filedir.
696         $vfileroot = $this->setup_vfile_root();
698         $this->expectException('file_exception');
699         $this->expectExceptionMessageRegExp(
700             '/Cannot read file\. Either the file does not exist or there is a permission problem\./');
702         $fs = new file_system_filedir();
703         $fs->add_file_from_path(\org\bovigo\vfs\vfsStream::url('filedir/file'));
704     }
706     /**
707      * Test that an appropriate error message is generated when specifying
708      * the wrong contenthash when adding a file to the pool.
709      */
710     public function test_add_file_from_path_mismatched_hash() {
711         $this->resetAfterTest();
713         $filecontent = 'example content';
714         $contenthash = file_storage::hash_from_string($filecontent);
715         $sourcedir = [
716             'file' => $filecontent,
717         ];
718         $vfileroot = $this->setup_vfile_root([], [], $sourcedir);
720         $fs = new file_system_filedir();
721         $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
722         $fs->add_file_from_path($filepath, 'eee4943847a35a4b6942c6f96daafde06bcfdfab');
723         $this->assertDebuggingCalled("Invalid contenthash submitted for file $filepath");
724     }
726     /**
727      * Test that an appropriate error message is generated when an existing
728      * file in the pool has the wrong contenthash
729      */
730     public function test_add_file_from_path_existing_content_invalid() {
731         $this->resetAfterTest();
733         $filecontent = 'example content';
734         $contenthash = file_storage::hash_from_string($filecontent);
735         $filedircontent = [
736             '0f' => [
737                 'f3' => [
738                     // This contains a virtual file which has a cache mismatch.
739                     '0ff30941ca5acd879fd809e8c937d9f9e6dd1615' => 'different example content',
740                 ],
741             ],
742         ];
743         $sourcedir = [
744             'file' => $filecontent,
745         ];
746         $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir);
748         // Check that we hit the jackpot.
749         $fs = new file_system_filedir();
750         $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
751         $result = $fs->add_file_from_path($filepath);
753         // We provided a bad hash. Check that the file was replaced.
754         $this->assertDebuggingCalled("Replacing invalid content file $contenthash");
756         // Test the output.
757         $this->assertEquals($contenthash, $result[0]);
758         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
759         $this->assertFalse($result[2]);
761         // Fetch the new file structure.
762         $structure = \org\bovigo\vfs\vfsStream::inspect(
763             new \org\bovigo\vfs\visitor\vfsStreamStructureVisitor()
764         )->getStructure();
766         $this->assertEquals($filecontent, $structure['root']['filedir']['0f']['f3'][$contenthash]);
767     }
769     /**
770      * Test that an appropriate error message is generated when adding a
771      * file to the pool when the pool directory structure is not writable.
772      */
773     public function test_add_file_from_path_existing_cannot_write_hashpath() {
774         $this->resetAfterTest();
776         $filecontent = 'example content';
777         $contenthash = file_storage::hash_from_string($filecontent);
778         $filedircontent = [
779             '0f' => [],
780         ];
781         $sourcedir = [
782             'file' => $filecontent,
783         ];
784         $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir);
786         // Make the target path readonly.
787         $vfileroot->getChild('filedir/0f')
788             ->chmod(0444)
789             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
791         $this->expectException('file_exception');
792         $this->expectExceptionMessageRegExp(
793             "/Can not create local file pool directories, please verify permissions in dataroot./");
795         // Attempt to add the file to the file pool.
796         $fs = new file_system_filedir();
797         $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
798         $fs->add_file_from_path($sourcefile);
799     }
801     /**
802      * Test adding a string to the pool.
803      */
804     public function test_add_file_from_string() {
805         $this->resetAfterTest();
806         global $CFG;
808         $filecontent = 'example content';
809         $contenthash = file_storage::hash_from_string($filecontent);
810         $vfileroot = $this->setup_vfile_root();
812         // Note, the vfs file system does not support locks - prevent file locking here.
813         $CFG->preventfilelocking = true;
815         // Attempt to add the file to the file pool.
816         $fs = new file_system_filedir();
817         $result = $fs->add_file_from_string($filecontent);
819         // Test the output.
820         $this->assertEquals($contenthash, $result[0]);
821         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
822         $this->assertTrue($result[2]);
823     }
825     /**
826      * Test that an appropriate error message is generated when adding a
827      * string to the pool when the pool directory structure is not writable.
828      */
829     public function test_add_file_from_string_existing_cannot_write_hashpath() {
830         $this->resetAfterTest();
832         $filecontent = 'example content';
833         $contenthash = file_storage::hash_from_string($filecontent);
835         $filedircontent = [
836             '0f' => [],
837         ];
838         $vfileroot = $this->setup_vfile_root($filedircontent);
840         // Make the target path readonly.
841         $vfileroot->getChild('filedir/0f')
842             ->chmod(0444)
843             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
845         $this->expectException('file_exception');
846         $this->expectExceptionMessageRegExp(
847             "/Can not create local file pool directories, please verify permissions in dataroot./");
849         // Attempt to add the file to the file pool.
850         $fs = new file_system_filedir();
851         $fs->add_file_from_string($filecontent);
852     }
854     /**
855      * Test adding a string to the pool when an item with the same
856      * contenthash is already present.
857      */
858     public function test_add_file_from_string_existing_matches() {
859         $this->resetAfterTest();
860         global $CFG;
862         $filecontent = 'example content';
863         $contenthash = file_storage::hash_from_string($filecontent);
864         $filedircontent = [
865             '0f' => [
866                 'f3' => [
867                     $contenthash => $filecontent,
868                 ],
869             ],
870         ];
872         $vfileroot = $this->setup_vfile_root($filedircontent);
874         // Note, the vfs file system does not support locks - prevent file locking here.
875         $CFG->preventfilelocking = true;
877         // Attempt to add the file to the file pool.
878         $fs = new file_system_filedir();
879         $result = $fs->add_file_from_string($filecontent);
881         // Test the output.
882         $this->assertEquals($contenthash, $result[0]);
883         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
884         $this->assertFalse($result[2]);
885     }
887     /**
888      * Test the cleanup of deleted files when there are no files to delete.
889      */
890     public function test_remove_file_missing() {
891         $this->resetAfterTest();
893         $filecontent = 'example content';
894         $contenthash = file_storage::hash_from_string($filecontent);
895         $vfileroot = $this->setup_vfile_root();
897         $fs = new file_system_filedir();
898         $fs->remove_file($contenthash);
900         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
901         // No file to move to trash, so the trash path will also be empty.
902         $this->assertFalse($vfileroot->hasChild('trashdir/0f'));
903         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3'));
904         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
905     }
907     /**
908      * Test the cleanup of deleted files when a file already exists in the
909      * trash for that path.
910      */
911     public function test_remove_file_existing_trash() {
912         $this->resetAfterTest();
914         $filecontent = 'example content';
915         $contenthash = file_storage::hash_from_string($filecontent);
917         $filedircontent = $trashdircontent = [
918             '0f' => [
919                 'f3' => [
920                     $contenthash => $filecontent,
921                 ],
922             ],
923         ];
924         $trashdircontent['0f']['f3'][$contenthash] .= 'different';
925         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
927         $fs = new file_system_filedir();
928         $fs->remove_file($contenthash);
930         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
931         $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
932         $this->assertNotEquals($filecontent, $vfileroot->getChild('trashdir/0f/f3/' . $contenthash)->getContent());
933     }
935     /**
936      * Ensure that remove_file does nothing with an empty file.
937      */
938     public function test_remove_file_empty() {
939         $this->resetAfterTest();
940         global $DB;
942         $DB = $this->getMockBuilder(\moodle_database::class)
943             ->setMethods(['record_exists'])
944             ->getMockForAbstractClass();
946         $DB->expects($this->never())
947             ->method('record_exists');
949         $fs = new file_system_filedir();
951         $result = $fs->remove_file(file_storage::hash_from_string(''));
952         $this->assertNull($result);
953     }
955     /**
956      * Ensure that remove_file does nothing when a file is still
957      * in use.
958      */
959     public function test_remove_file_in_use() {
960         $this->resetAfterTest();
961         global $DB;
963         $filecontent = 'example content';
964         $contenthash = file_storage::hash_from_string($filecontent);
965         $filedircontent = [
966             '0f' => [
967                 'f3' => [
968                     $contenthash => $filecontent,
969                 ],
970             ],
971         ];
972         $vfileroot = $this->setup_vfile_root($filedircontent);
974         $DB = $this->getMockBuilder(\moodle_database::class)
975             ->setMethods(['record_exists'])
976             ->getMockForAbstractClass();
978         $DB->method('record_exists')->willReturn(true);
980         $fs = new file_system_filedir();
981         $result = $fs->remove_file($contenthash);
982         $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
983         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
984     }
986     /**
987      * Ensure that remove_file removes the file when it is no
988      * longer in use.
989      */
990     public function test_remove_file_expired() {
991         $this->resetAfterTest();
992         global $DB;
994         $filecontent = 'example content';
995         $contenthash = file_storage::hash_from_string($filecontent);
996         $filedircontent = [
997             '0f' => [
998                 'f3' => [
999                     $contenthash => $filecontent,
1000                 ],
1001             ],
1002         ];
1003         $vfileroot = $this->setup_vfile_root($filedircontent);
1005         $DB = $this->getMockBuilder(\moodle_database::class)
1006             ->setMethods(['record_exists'])
1007             ->getMockForAbstractClass();
1009         $DB->method('record_exists')->willReturn(false);
1011         $fs = new file_system_filedir();
1012         $result = $fs->remove_file($contenthash);
1013         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1014         $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1015     }
1017     /**
1018      * Test purging the cache.
1019      */
1020     public function test_empty_trash() {
1021         $this->resetAfterTest();
1023         $filecontent = 'example content';
1024         $contenthash = file_storage::hash_from_string($filecontent);
1026         $filedircontent = $trashdircontent = [
1027             '0f' => [
1028                 'f3' => [
1029                     $contenthash => $filecontent,
1030                 ],
1031             ],
1032         ];
1033         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
1035         $fs = new file_system_filedir();
1036         $method = new ReflectionMethod(file_system_filedir::class, 'empty_trash');
1037         $method->setAccessible(true);
1038         $result = $method->invoke($fs);
1040         $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1041         $this->assertFalse($vfileroot->hasChild('trashdir'));
1042         $this->assertFalse($vfileroot->hasChild('trashdir/0f'));
1043         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3'));
1044         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1045     }
1047     /**
1048      * Data Provider for contenthash to contendir conversion.
1049      *
1050      * @return  array
1051      */
1052     public function contenthash_dataprovider() {
1053         return array(
1054             array(
1055                 'contenthash'   => 'eee4943847a35a4b6942c6f96daafde06bcfdfab',
1056                 'contentdir'    => 'ee/e4',
1057             ),
1058             array(
1059                 'contenthash'   => 'aef05a62ae81ca0005d2569447779af062b7cda0',
1060                 'contentdir'    => 'ae/f0',
1061             ),
1062         );
1063     }