MDL-58297 filestorage: New functions for hash calculation
[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->setExpectedExceptionRegexp('file_exception',
153             '/Can not create local file pool directories, please verify permissions in dataroot./');
155         new file_system_filedir();
156     }
158     /**
159      * Ensure that an appropriate error is shown when the trash directory
160      * is not writable.
161      */
162     public function test_readonly_filesystem_trashdir() {
163         $this->resetAfterTest();
165         // Setup the trashdir but remove permissions.
166         $vfileroot = $this->setup_vfile_root([], null);
168         // Make the target path readonly.
169         $vfileroot->chmod(0444)
170             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
172         // This should generate an exception.
173         $this->setExpectedExceptionRegexp('file_exception',
174             '/Can not create local file pool directories, please verify permissions in dataroot./');
176         new file_system_filedir();
177     }
179     /**
180      * Test that the standard Moodle warning message is put into the filedir.
181      */
182     public function test_warnings_put_in_place() {
183         $this->resetAfterTest();
185         $vfileroot = $this->setup_vfile_root(null);
187         new file_system_filedir();
188         $this->assertTrue($vfileroot->hasChild('filedir/warning.txt'));
189         $this->assertEquals(
190             'This directory contains the content of uploaded files and is controlled by Moodle code. ' .
191                 'Do not manually move, change or rename any of the files and subdirectories here.',
192             $vfileroot->getChild('filedir/warning.txt')->getContent()
193         );
194     }
196     /**
197      * Ensure that the default implementation of get_remote_path_from_hash
198      * simply calls get_local_path_from_hash.
199      */
200     public function test_get_remote_path_from_hash() {
201         $filecontent = 'example content';
202         $contenthash = file_storage::hash_from_string($filecontent);
203         $expectedresult = (object) [];
205         $fs = $this->get_testable_mock([
206             'get_local_path_from_hash',
207         ]);
209         $fs->expects($this->once())
210             ->method('get_local_path_from_hash')
211             ->with($this->equalTo($contenthash), $this->equalTo(false))
212             ->willReturn($expectedresult);
214         $method = new ReflectionMethod(file_system_filedir::class, 'get_remote_path_from_hash');
215         $method->setAccessible(true);
216         $result = $method->invokeArgs($fs, [$contenthash]);
218         $this->assertEquals($expectedresult, $result);
219     }
221     /**
222      * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
223      * a failed recovery.
224      */
225     public function test_get_local_path_from_storedfile_with_recovery() {
226         $filecontent = 'example content';
227         $file = $this->get_stored_file($filecontent);
228         $fs = $this->get_testable_mock([
229             'get_local_path_from_hash',
230             'recover_file',
231         ]);
232         $filepath = '/path/to/nonexistent/file';
234         $fs->method('get_local_path_from_hash')
235             ->willReturn($filepath);
237         $fs->expects($this->once())
238             ->method('recover_file')
239             ->with($this->equalTo($file));
241         $file = $this->get_stored_file('example content');
242         $method = new ReflectionMethod(file_system_filedir::class, 'get_local_path_from_storedfile');
243         $method->setAccessible(true);
244         $result = $method->invokeArgs($fs, array($file, true));
246         $this->assertEquals($filepath, $result);
247     }
249     /**
250      * Test the stock implementation of get_local_path_from_storedfile_with_recovery with no file found and
251      * a failed recovery.
252      */
253     public function test_get_local_path_from_storedfile_without_recovery() {
254         $filecontent = 'example content';
255         $file = $this->get_stored_file($filecontent);
256         $fs = $this->get_testable_mock([
257             'get_local_path_from_hash',
258             'recover_file',
259         ]);
260         $filepath = '/path/to/nonexistent/file';
262         $fs->method('get_local_path_from_hash')
263             ->willReturn($filepath);
265         $fs->expects($this->never())
266             ->method('recover_file');
268         $file = $this->get_stored_file('example content');
269         $method = new ReflectionMethod(file_system_filedir::class, 'get_local_path_from_storedfile');
270         $method->setAccessible(true);
271         $result = $method->invokeArgs($fs, array($file, false));
273         $this->assertEquals($filepath, $result);
274     }
276     /**
277      * Test that the correct path is generated for the supplied content
278      * hashes.
279      *
280      * @dataProvider contenthash_dataprovider
281      * @param   string  $hash contenthash to test
282      * @param   string  $hashdir Expected format of content directory
283      */
284     public function test_get_fulldir_from_hash($hash, $hashdir) {
285         global $CFG;
287         $fs = new file_system_filedir();
288         $method = new ReflectionMethod(file_system_filedir::class, 'get_fulldir_from_hash');
289         $method->setAccessible(true);
290         $result = $method->invokeArgs($fs, array($hash));
292         $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir);
293         $this->assertEquals($expectedpath, $result);
294     }
296     /**
297      * Test that the correct path is generated for the supplied content
298      * hashes when used with a stored_file.
299      *
300      * @dataProvider contenthash_dataprovider
301      * @param   string  $hash contenthash to test
302      * @param   string  $hashdir Expected format of content directory
303      */
304     public function test_get_fulldir_from_storedfile($hash, $hashdir) {
305         global $CFG;
307         $file = $this->getMockBuilder('stored_file')
308             ->disableOriginalConstructor()
309             ->setMethods([
310                 'sync_external_file',
311                 'get_contenthash',
312             ])
313             ->getMock();
315         $file->method('get_contenthash')->willReturn($hash);
317         $fs = new file_system_filedir();
318         $method = new ReflectionMethod('file_system_filedir', 'get_fulldir_from_storedfile');
319         $method->setAccessible(true);
320         $result = $method->invokeArgs($fs, array($file));
322         $expectedpath = sprintf('%s/filedir/%s', $CFG->dataroot, $hashdir);
323         $this->assertEquals($expectedpath, $result);
324     }
326     /**
327      * Test that the correct content directory is generated for the supplied
328      * content hashes.
329      *
330      * @dataProvider contenthash_dataprovider
331      * @param   string  $hash contenthash to test
332      * @param   string  $hashdir Expected format of content directory
333      */
334     public function test_get_contentdir_from_hash($hash, $hashdir) {
335         $method = new ReflectionMethod(file_system_filedir::class, 'get_contentdir_from_hash');
336         $method->setAccessible(true);
338         $fs = new file_system_filedir();
339         $result = $method->invokeArgs($fs, array($hash));
341         $this->assertEquals($hashdir, $result);
342     }
344     /**
345      * Test that the correct content path is generated for the supplied
346      * content hashes.
347      *
348      * @dataProvider contenthash_dataprovider
349      * @param   string  $hash contenthash to test
350      * @param   string  $hashdir Expected format of content directory
351      */
352     public function test_get_contentpath_from_hash($hash, $hashdir) {
353         $method = new ReflectionMethod(file_system_filedir::class, 'get_contentpath_from_hash');
354         $method->setAccessible(true);
356         $fs = new file_system_filedir();
357         $result = $method->invokeArgs($fs, array($hash));
359         $expectedpath = sprintf('%s/%s', $hashdir, $hash);
360         $this->assertEquals($expectedpath, $result);
361     }
363     /**
364      * Test that the correct trash path is generated for the supplied
365      * content hashes.
366      *
367      * @dataProvider contenthash_dataprovider
368      * @param   string  $hash contenthash to test
369      * @param   string  $hashdir Expected format of content directory
370      */
371     public function test_get_trash_fullpath_from_hash($hash, $hashdir) {
372         global $CFG;
374         $fs = new file_system_filedir();
375         $method = new ReflectionMethod(file_system_filedir::class, 'get_trash_fullpath_from_hash');
376         $method->setAccessible(true);
377         $result = $method->invokeArgs($fs, array($hash));
379         $expectedpath = sprintf('%s/trashdir/%s/%s', $CFG->dataroot, $hashdir, $hash);
380         $this->assertEquals($expectedpath, $result);
381     }
383     /**
384      * Test that the correct trash directory is generated for the supplied
385      * content hashes.
386      *
387      * @dataProvider contenthash_dataprovider
388      * @param   string  $hash contenthash to test
389      * @param   string  $hashdir Expected format of content directory
390      */
391     public function test_get_trash_fulldir_from_hash($hash, $hashdir) {
392         global $CFG;
394         $fs = new file_system_filedir();
395         $method = new ReflectionMethod(file_system_filedir::class, 'get_trash_fulldir_from_hash');
396         $method->setAccessible(true);
397         $result = $method->invokeArgs($fs, array($hash));
399         $expectedpath = sprintf('%s/trashdir/%s', $CFG->dataroot, $hashdir);
400         $this->assertEquals($expectedpath, $result);
401     }
403     /**
404      * Ensure that copying a file to a target from a stored_file works as anticipated.
405      */
406     public function test_copy_content_from_storedfile() {
407         $this->resetAfterTest();
408         global $CFG;
410         $filecontent = 'example content';
411         $contenthash = file_storage::hash_from_string($filecontent);
412         $filedircontent = [
413             $contenthash => $filecontent,
414         ];
415         $vfileroot = $this->setup_vfile_root($filedircontent, [], []);
417         $fs = $this->getMockBuilder(file_system_filedir::class)
418             ->disableOriginalConstructor()
419             ->setMethods([
420                 'get_local_path_from_storedfile',
421             ])
422             ->getMock();
424         $file = $this->getMockBuilder(stored_file::class)
425             ->disableOriginalConstructor()
426             ->getMock();
428         $sourcefile = \org\bovigo\vfs\vfsStream::url('root/filedir/' . $contenthash);
429         $fs->method('get_local_path_from_storedfile')->willReturn($sourcefile);
431         $targetfile = \org\bovigo\vfs\vfsStream::url('root/targetfile');
432         $CFG->preventfilelocking = true;
433         $result = $fs->copy_content_from_storedfile($file, $targetfile);
435         $this->assertTrue($result);
436         $this->assertEquals($filecontent, $vfileroot->getChild('targetfile')->getContent());
437     }
439     /**
440      * Ensure that content recovery works.
441      */
442     public function test_recover_file() {
443         $this->resetAfterTest();
445         // Setup the filedir.
446         // This contains a virtual file which has a cache mismatch.
447         $filecontent = 'example content';
448         $contenthash = file_storage::hash_from_string($filecontent);
450         $trashdircontent = [
451             '0f' => [
452                 'f3' => [
453                     $contenthash => $filecontent,
454                 ],
455             ],
456         ];
458         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
460         $file = new stored_file(get_file_storage(), (object) [
461             'contenthash' => $contenthash,
462             'filesize' => strlen($filecontent),
463         ]);
465         $fs = new file_system_filedir();
466         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
467         $method->setAccessible(true);
468         $result = $method->invokeArgs($fs, array($file));
470         // Test the output.
471         $this->assertTrue($result);
473         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
475     }
477     /**
478      * Ensure that content recovery works.
479      */
480     public function test_recover_file_already_present() {
481         $this->resetAfterTest();
483         // Setup the filedir.
484         // This contains a virtual file which has a cache mismatch.
485         $filecontent = 'example content';
486         $contenthash = file_storage::hash_from_string($filecontent);
488         $filedircontent = $trashdircontent = [
489             '0f' => [
490                 'f3' => [
491                     $contenthash => $filecontent,
492                 ],
493             ],
494         ];
496         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
498         $file = new stored_file(get_file_storage(), (object) [
499             'contenthash' => $contenthash,
500             'filesize' => strlen($filecontent),
501         ]);
503         $fs = new file_system_filedir();
504         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
505         $method->setAccessible(true);
506         $result = $method->invokeArgs($fs, array($file));
508         // Test the output.
509         $this->assertTrue($result);
511         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
512     }
514     /**
515      * Ensure that content recovery works.
516      */
517     public function test_recover_file_size_mismatch() {
518         $this->resetAfterTest();
520         // Setup the filedir.
521         // This contains a virtual file which has a cache mismatch.
522         $filecontent = 'example content';
523         $contenthash = file_storage::hash_from_string($filecontent);
525         $trashdircontent = [
526             '0f' => [
527                 'f3' => [
528                     $contenthash => $filecontent,
529                 ],
530             ],
531         ];
532         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
534         $file = new stored_file(get_file_storage(), (object) [
535             'contenthash' => $contenthash,
536             'filesize' => strlen($filecontent) + 1,
537         ]);
539         $fs = new file_system_filedir();
540         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
541         $method->setAccessible(true);
542         $result = $method->invokeArgs($fs, array($file));
544         // Test the output.
545         $this->assertFalse($result);
546         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
547     }
549     /**
550      * Ensure that content recovery works.
551      */
552     public function test_recover_file_has_mismatch() {
553         $this->resetAfterTest();
555         // Setup the filedir.
556         // This contains a virtual file which has a cache mismatch.
557         $filecontent = 'example content';
558         $contenthash = file_storage::hash_from_string($filecontent);
560         $trashdircontent = [
561             '0f' => [
562                 'f3' => [
563                     $contenthash => $filecontent,
564                 ],
565             ],
566         ];
567         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
569         $file = new stored_file(get_file_storage(), (object) [
570             'contenthash' => $contenthash . " different",
571             'filesize' => strlen($filecontent),
572         ]);
574         $fs = new file_system_filedir();
575         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
576         $method->setAccessible(true);
577         $result = $method->invokeArgs($fs, array($file));
579         // Test the output.
580         $this->assertFalse($result);
581         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
582     }
584     /**
585      * Ensure that content recovery works when the content file is in the
586      * alt trash directory.
587      */
588     public function test_recover_file_alttrash() {
589         $this->resetAfterTest();
591         // Setup the filedir.
592         // This contains a virtual file which has a cache mismatch.
593         $filecontent = 'example content';
594         $contenthash = file_storage::hash_from_string($filecontent);
596         $trashdircontent = [
597             $contenthash => $filecontent,
598         ];
599         $vfileroot = $this->setup_vfile_root([], $trashdircontent);
601         $file = new stored_file(get_file_storage(), (object) [
602             'contenthash' => $contenthash,
603             'filesize' => strlen($filecontent),
604         ]);
606         $fs = new file_system_filedir();
607         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
608         $method->setAccessible(true);
609         $result = $method->invokeArgs($fs, array($file));
611         // Test the output.
612         $this->assertTrue($result);
614         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
615     }
617     /**
618      * Test that an appropriate error message is generated when adding a
619      * file to the pool when the pool directory structure is not writable.
620      */
621     public function test_recover_file_contentdir_readonly() {
622         $this->resetAfterTest();
624         $filecontent = 'example content';
625         $contenthash = file_storage::hash_from_string($filecontent);
626         $filedircontent = [
627             '0f' => [],
628         ];
629         $trashdircontent = [
630             $contenthash => $filecontent,
631         ];
632         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
634         // Make the target path readonly.
635         $vfileroot->getChild('filedir/0f')
636             ->chmod(0444)
637             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
639         $file = new stored_file(get_file_storage(), (object) [
640             'contenthash' => $contenthash,
641             'filesize' => strlen($filecontent),
642         ]);
644         $fs = new file_system_filedir();
645         $method = new ReflectionMethod(file_system_filedir::class, 'recover_file');
646         $method->setAccessible(true);
647         $result = $method->invokeArgs($fs, array($file));
649         // Test the output.
650         $this->assertFalse($result);
651     }
653     /**
654      * Test adding a file to the pool.
655      */
656     public function test_add_file_from_path() {
657         $this->resetAfterTest();
658         global $CFG;
660         // Setup the filedir.
661         // This contains a virtual file which has a cache mismatch.
662         $filecontent = 'example content';
663         $contenthash = file_storage::hash_from_string($filecontent);
664         $sourcedircontent = [
665             'file' => $filecontent,
666         ];
668         $vfileroot = $this->setup_vfile_root([], [], $sourcedircontent);
670         // Note, the vfs file system does not support locks - prevent file locking here.
671         $CFG->preventfilelocking = true;
673         // Attempt to add the file to the file pool.
674         $fs = new file_system_filedir();
675         $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
676         $result = $fs->add_file_from_path($sourcefile);
678         // Test the output.
679         $this->assertEquals($contenthash, $result[0]);
680         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
681         $this->assertTrue($result[2]);
683         $this->assertEquals($filecontent, $vfileroot->getChild('filedir/0f/f3/' . $contenthash)->getContent());
684     }
686     /**
687      * Test that an appropriate error message is generated when adding an
688      * unavailable file to the pool is attempted.
689      */
690     public function test_add_file_from_path_file_unavailable() {
691         $this->resetAfterTest();
693         // Setup the filedir.
694         $vfileroot = $this->setup_vfile_root();
696         $this->setExpectedExceptionRegexp('file_exception',
697             '/Cannot read file\. Either the file does not exist or there is a permission problem\./');
699         $fs = new file_system_filedir();
700         $fs->add_file_from_path(\org\bovigo\vfs\vfsStream::url('filedir/file'));
701     }
703     /**
704      * Test that an appropriate error message is generated when specifying
705      * the wrong contenthash when adding a file to the pool.
706      */
707     public function test_add_file_from_path_mismatched_hash() {
708         $this->resetAfterTest();
710         $filecontent = 'example content';
711         $contenthash = file_storage::hash_from_string($filecontent);
712         $sourcedir = [
713             'file' => $filecontent,
714         ];
715         $vfileroot = $this->setup_vfile_root([], [], $sourcedir);
717         $fs = new file_system_filedir();
718         $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
719         $fs->add_file_from_path($filepath, 'eee4943847a35a4b6942c6f96daafde06bcfdfab');
720         $this->assertDebuggingCalled("Invalid contenthash submitted for file $filepath");
721     }
723     /**
724      * Test that an appropriate error message is generated when an existing
725      * file in the pool has the wrong contenthash
726      */
727     public function test_add_file_from_path_existing_content_invalid() {
728         $this->resetAfterTest();
730         $filecontent = 'example content';
731         $contenthash = file_storage::hash_from_string($filecontent);
732         $filedircontent = [
733             '0f' => [
734                 'f3' => [
735                     // This contains a virtual file which has a cache mismatch.
736                     '0ff30941ca5acd879fd809e8c937d9f9e6dd1615' => 'different example content',
737                 ],
738             ],
739         ];
740         $sourcedir = [
741             'file' => $filecontent,
742         ];
743         $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir);
745         // Check that we hit the jackpot.
746         $fs = new file_system_filedir();
747         $filepath = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
748         $result = $fs->add_file_from_path($filepath);
750         // We provided a bad hash. Check that the file was replaced.
751         $this->assertDebuggingCalled("Replacing invalid content file $contenthash");
753         // Test the output.
754         $this->assertEquals($contenthash, $result[0]);
755         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
756         $this->assertFalse($result[2]);
758         // Fetch the new file structure.
759         $structure = \org\bovigo\vfs\vfsStream::inspect(
760             new \org\bovigo\vfs\visitor\vfsStreamStructureVisitor()
761         )->getStructure();
763         $this->assertEquals($filecontent, $structure['root']['filedir']['0f']['f3'][$contenthash]);
764     }
766     /**
767      * Test that an appropriate error message is generated when adding a
768      * file to the pool when the pool directory structure is not writable.
769      */
770     public function test_add_file_from_path_existing_cannot_write_hashpath() {
771         $this->resetAfterTest();
773         $filecontent = 'example content';
774         $contenthash = file_storage::hash_from_string($filecontent);
775         $filedircontent = [
776             '0f' => [],
777         ];
778         $sourcedir = [
779             'file' => $filecontent,
780         ];
781         $vfileroot = $this->setup_vfile_root($filedircontent, [], $sourcedir);
783         // Make the target path readonly.
784         $vfileroot->getChild('filedir/0f')
785             ->chmod(0444)
786             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
788         $this->setExpectedException(
789             'file_exception',
790             "Can not create local file pool directories, please verify permissions in dataroot."
791         );
793         // Attempt to add the file to the file pool.
794         $fs = new file_system_filedir();
795         $sourcefile = \org\bovigo\vfs\vfsStream::url('root/sourcedir/file');
796         $fs->add_file_from_path($sourcefile);
797     }
799     /**
800      * Test adding a string to the pool.
801      */
802     public function test_add_file_from_string() {
803         $this->resetAfterTest();
804         global $CFG;
806         $filecontent = 'example content';
807         $contenthash = file_storage::hash_from_string($filecontent);
808         $vfileroot = $this->setup_vfile_root();
810         // Note, the vfs file system does not support locks - prevent file locking here.
811         $CFG->preventfilelocking = true;
813         // Attempt to add the file to the file pool.
814         $fs = new file_system_filedir();
815         $result = $fs->add_file_from_string($filecontent);
817         // Test the output.
818         $this->assertEquals($contenthash, $result[0]);
819         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
820         $this->assertTrue($result[2]);
821     }
823     /**
824      * Test that an appropriate error message is generated when adding a
825      * string to the pool when the pool directory structure is not writable.
826      */
827     public function test_add_file_from_string_existing_cannot_write_hashpath() {
828         $this->resetAfterTest();
830         $filecontent = 'example content';
831         $contenthash = file_storage::hash_from_string($filecontent);
833         $filedircontent = [
834             '0f' => [],
835         ];
836         $vfileroot = $this->setup_vfile_root($filedircontent);
838         // Make the target path readonly.
839         $vfileroot->getChild('filedir/0f')
840             ->chmod(0444)
841             ->chown(\org\bovigo\vfs\vfsStream::OWNER_USER_2);
843         $this->setExpectedException(
844             'file_exception',
845             "Can not create local file pool directories, please verify permissions in dataroot."
846         );
848         // Attempt to add the file to the file pool.
849         $fs = new file_system_filedir();
850         $fs->add_file_from_string($filecontent);
851     }
853     /**
854      * Test adding a string to the pool when an item with the same
855      * contenthash is already present.
856      */
857     public function test_add_file_from_string_existing_matches() {
858         $this->resetAfterTest();
859         global $CFG;
861         $filecontent = 'example content';
862         $contenthash = file_storage::hash_from_string($filecontent);
863         $filedircontent = [
864             '0f' => [
865                 'f3' => [
866                     $contenthash => $filecontent,
867                 ],
868             ],
869         ];
871         $vfileroot = $this->setup_vfile_root($filedircontent);
873         // Note, the vfs file system does not support locks - prevent file locking here.
874         $CFG->preventfilelocking = true;
876         // Attempt to add the file to the file pool.
877         $fs = new file_system_filedir();
878         $result = $fs->add_file_from_string($filecontent);
880         // Test the output.
881         $this->assertEquals($contenthash, $result[0]);
882         $this->assertEquals(core_text::strlen($filecontent), $result[1]);
883         $this->assertFalse($result[2]);
884     }
886     /**
887      * Test the cleanup of deleted files when there are no files to delete.
888      */
889     public function test_remove_file_missing() {
890         $this->resetAfterTest();
892         $filecontent = 'example content';
893         $contenthash = file_storage::hash_from_string($filecontent);
894         $vfileroot = $this->setup_vfile_root();
896         $fs = new file_system_filedir();
897         $fs->remove_file($contenthash);
899         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
900         // No file to move to trash, so the trash path will also be empty.
901         $this->assertFalse($vfileroot->hasChild('trashdir/0f'));
902         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3'));
903         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
904     }
906     /**
907      * Test the cleanup of deleted files when a file already exists in the
908      * trash for that path.
909      */
910     public function test_remove_file_existing_trash() {
911         $this->resetAfterTest();
913         $filecontent = 'example content';
914         $contenthash = file_storage::hash_from_string($filecontent);
916         $filedircontent = $trashdircontent = [
917             '0f' => [
918                 'f3' => [
919                     $contenthash => $filecontent,
920                 ],
921             ],
922         ];
923         $trashdircontent['0f']['f3'][$contenthash] .= 'different';
924         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
926         $fs = new file_system_filedir();
927         $fs->remove_file($contenthash);
929         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
930         $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
931         $this->assertNotEquals($filecontent, $vfileroot->getChild('trashdir/0f/f3/' . $contenthash)->getContent());
932     }
934     /**
935      * Ensure that remove_file does nothing with an empty file.
936      */
937     public function test_remove_file_empty() {
938         $this->resetAfterTest();
939         global $DB;
941         $DB = $this->getMockBuilder(\moodle_database::class)
942             ->setMethods(['record_exists'])
943             ->getMockForAbstractClass();
945         $DB->expects($this->never())
946             ->method('record_exists');
948         $fs = new file_system_filedir();
950         $result = $fs->remove_file(file_storage::hash_from_string(''));
951         $this->assertNull($result);
952     }
954     /**
955      * Ensure that remove_file does nothing when a file is still
956      * in use.
957      */
958     public function test_remove_file_in_use() {
959         $this->resetAfterTest();
960         global $DB;
962         $filecontent = 'example content';
963         $contenthash = file_storage::hash_from_string($filecontent);
964         $filedircontent = [
965             '0f' => [
966                 'f3' => [
967                     $contenthash => $filecontent,
968                 ],
969             ],
970         ];
971         $vfileroot = $this->setup_vfile_root($filedircontent);
973         $DB = $this->getMockBuilder(\moodle_database::class)
974             ->setMethods(['record_exists'])
975             ->getMockForAbstractClass();
977         $DB->method('record_exists')->willReturn(true);
979         $fs = new file_system_filedir();
980         $result = $fs->remove_file($contenthash);
981         $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
982         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
983     }
985     /**
986      * Ensure that remove_file removes the file when it is no
987      * longer in use.
988      */
989     public function test_remove_file_expired() {
990         $this->resetAfterTest();
991         global $DB;
993         $filecontent = 'example content';
994         $contenthash = file_storage::hash_from_string($filecontent);
995         $filedircontent = [
996             '0f' => [
997                 'f3' => [
998                     $contenthash => $filecontent,
999                 ],
1000             ],
1001         ];
1002         $vfileroot = $this->setup_vfile_root($filedircontent);
1004         $DB = $this->getMockBuilder(\moodle_database::class)
1005             ->setMethods(['record_exists'])
1006             ->getMockForAbstractClass();
1008         $DB->method('record_exists')->willReturn(false);
1010         $fs = new file_system_filedir();
1011         $result = $fs->remove_file($contenthash);
1012         $this->assertFalse($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1013         $this->assertTrue($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1014     }
1016     /**
1017      * Test purging the cache.
1018      */
1019     public function test_empty_trash() {
1020         $this->resetAfterTest();
1022         $filecontent = 'example content';
1023         $contenthash = file_storage::hash_from_string($filecontent);
1025         $filedircontent = $trashdircontent = [
1026             '0f' => [
1027                 'f3' => [
1028                     $contenthash => $filecontent,
1029                 ],
1030             ],
1031         ];
1032         $vfileroot = $this->setup_vfile_root($filedircontent, $trashdircontent);
1034         $fs = new file_system_filedir();
1035         $method = new ReflectionMethod(file_system_filedir::class, 'empty_trash');
1036         $method->setAccessible(true);
1037         $result = $method->invoke($fs);
1039         $this->assertTrue($vfileroot->hasChild('filedir/0f/f3/' . $contenthash));
1040         $this->assertFalse($vfileroot->hasChild('trashdir'));
1041         $this->assertFalse($vfileroot->hasChild('trashdir/0f'));
1042         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3'));
1043         $this->assertFalse($vfileroot->hasChild('trashdir/0f/f3/' . $contenthash));
1044     }
1046     /**
1047      * Data Provider for contenthash to contendir conversion.
1048      *
1049      * @return  array
1050      */
1051     public function contenthash_dataprovider() {
1052         return array(
1053             array(
1054                 'contenthash'   => 'eee4943847a35a4b6942c6f96daafde06bcfdfab',
1055                 'contentdir'    => 'ee/e4',
1056             ),
1057             array(
1058                 'contenthash'   => 'aef05a62ae81ca0005d2569447779af062b7cda0',
1059                 'contentdir'    => 'ae/f0',
1060             ),
1061         );
1062     }