19fdfb6b83d0dac4e00b106c65e6c77e119ef6c4
[moodle.git] / cache / stores / file / lib.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  * The library file for the file cache store.
19  *
20  * This file is part of the file cache store, it contains the API for interacting with an instance of the store.
21  * This is used as a default cache store within the Cache API. It should never be deleted.
22  *
23  * @package    cachestore_file
24  * @category   cache
25  * @copyright  2012 Sam Hemelryk
26  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27  */
29 /**
30  * The file store class.
31  *
32  * Configuration options
33  *      path:           string: path to the cache directory, if left empty one will be created in the cache directory
34  *      autocreate:     true, false
35  *      prescan:        true, false
36  *
37  * @copyright  2012 Sam Hemelryk
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class cachestore_file implements cache_store, cache_is_key_aware {
42     /**
43      * The name of the store.
44      * @var string
45      */
46     protected $name;
48     /**
49      * The path to use for the file storage.
50      * @var string
51      */
52     protected $path = null;
54     /**
55      * Set to true when a prescan has been performed.
56      * @var bool
57      */
58     protected $prescan = false;
60     /**
61      * Set to true if we should store files within a single directory.
62      * By default we use a nested structure in order to reduce the chance of conflicts and avoid any file system
63      * limitations such as maximum files per directory.
64      * @var bool
65      */
66     protected $singledirectory = false;
68     /**
69      * Set to true when the path should be automatically created if it does not yet exist.
70      * @var bool
71      */
72     protected $autocreate = false;
74     /**
75      * Set to true if a custom path is being used.
76      * @var bool
77      */
78     protected $custompath = false;
80     /**
81      * An array of keys we are sure about presently.
82      * @var array
83      */
84     protected $keys = array();
86     /**
87      * True when the store is ready to be initialised.
88      * @var bool
89      */
90     protected $isready = false;
92     /**
93      * The cache definition this instance has been initialised with.
94      * @var cache_definition
95      */
96     protected $definition;
98     /**
99      * Constructs the store instance.
100      *
101      * Noting that this function is not an initialisation. It is used to prepare the store for use.
102      * The store will be initialised when required and will be provided with a cache_definition at that time.
103      *
104      * @param string $name
105      * @param array $configuration
106      */
107     public function __construct($name, array $configuration = array()) {
108         $this->name = $name;
109         if (array_key_exists('path', $configuration) && $configuration['path'] !== '') {
110             $this->custompath = true;
111             $this->autocreate = !empty($configuration['autocreate']);
112             $path = (string)$configuration['path'];
113             if (!is_dir($path)) {
114                 if ($this->autocreate) {
115                     if (!make_writable_directory($path, false)) {
116                         $path = false;
117                         debugging('Error trying to autocreate file store path. '.$path, DEBUG_DEVELOPER);
118                     }
119                 } else {
120                     $path = false;
121                     debugging('The given file cache store path does not exist. '.$path, DEBUG_DEVELOPER);
122                 }
123             }
124             if ($path !== false && !is_writable($path)) {
125                 $path = false;
126                 debugging('The given file cache store path is not writable. '.$path, DEBUG_DEVELOPER);
127             }
128         } else {
129             $path = make_cache_directory('cachestore_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name));
130         }
131         $this->isready = $path !== false;
132         $this->path = $path;
133         // Check if we should prescan the directory.
134         if (array_key_exists('prescan', $configuration)) {
135             $this->prescan = (bool)$configuration['prescan'];
136         } else {
137             // Default is no, we should not prescan.
138             $this->prescan = false;
139         }
140         // Check if we should be storing in a single directory.
141         if (array_key_exists('singledirectory', $configuration)) {
142             $this->singledirectory = (bool)$configuration['singledirectory'];
143         } else {
144             // Default: No, we will use multiple directories.
145             $this->singledirectory = false;
146         }
147     }
149     /**
150      * Returns true if this store instance is ready to be used.
151      * @return bool
152      */
153     public function is_ready() {
154         return ($this->path !== null);
155     }
157     /**
158      * Returns true once this instance has been initialised.
159      *
160      * @return bool
161      */
162     public function is_initialised() {
163         return true;
164     }
166     /**
167      * Returns the supported features as a combined int.
168      *
169      * @param array $configuration
170      * @return int
171      */
172     public static function get_supported_features(array $configuration = array()) {
173         $supported = self::SUPPORTS_DATA_GUARANTEE +
174                      self::SUPPORTS_NATIVE_TTL;
175         return $supported;
176     }
178     /**
179      * Returns the supported modes as a combined int.
180      *
181      * @param array $configuration
182      * @return int
183      */
184     public static function get_supported_modes(array $configuration = array()) {
185         return self::MODE_APPLICATION + self::MODE_SESSION;
186     }
188     /**
189      * Returns true if the store requirements are met.
190      *
191      * @return bool
192      */
193     public static function are_requirements_met() {
194         return true;
195     }
197     /**
198      * Returns true if the given mode is supported by this store.
199      *
200      * @param int $mode One of cache_store::MODE_*
201      * @return bool
202      */
203     public static function is_supported_mode($mode) {
204         return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
205     }
207     /**
208      * Returns true if the store instance supports multiple identifiers.
209      *
210      * @return bool
211      */
212     public function supports_multiple_identifiers() {
213         return false;
214     }
216     /**
217      * Returns true if the store instance guarantees data.
218      *
219      * @return bool
220      */
221     public function supports_data_guarantee() {
222         return true;
223     }
225     /**
226      * Returns true if the store instance supports native ttl.
227      *
228      * @return bool
229      */
230     public function supports_native_ttl() {
231         return true;
232     }
234     /**
235      * Initialises the cache.
236      *
237      * Once this has been done the cache is all set to be used.
238      *
239      * @param cache_definition $definition
240      */
241     public function initialise(cache_definition $definition) {
242         $this->definition = $definition;
243         $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
244         $this->path .= '/'.$hash;
245         make_writable_directory($this->path);
246         if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
247             $this->prescan = false;
248         }
249         if ($this->prescan) {
250             $this->prescan_keys();
251         }
252     }
254     /**
255      * Pre-scan the cache to see which keys are present.
256      */
257     protected function prescan_keys() {
258         $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
259         if (is_array($files)) {
260             foreach ($files as $filename) {
261                 $this->keys[basename($filename)] = filemtime($filename);
262             }
263         }
264     }
266     /**
267      * Gets a pattern suitable for use with glob to find all keys in the cache.
268      * @return string The pattern.
269      */
270     protected function glob_keys_pattern() {
271         if ($this->singledirectory) {
272             return $this->path . '/*.cache';
273         } else {
274             return $this->path . '/*/*.cache';
275         }
276     }
278     /**
279      * Returns the file path to use for the given key.
280      *
281      * @param string $key The key to generate a file path for.
282      * @param bool $create If set to the true the directory structure the key requires will be created.
283      * @return string The full path to the file that stores a particular cache key.
284      */
285     protected function file_path_for_key($key, $create = false) {
286         if ($this->singledirectory) {
287             // Its a single directory, easy, just the store instances path + the file name.
288             return $this->path . '/' . $key . '.cache';
289         } else {
290             // We are using a single subdirectory to achieve 1 level.
291             $subdir = substr($key, 0, 3);
292             $dir = $this->path . '/' . $subdir;
293             if ($create) {
294                 // Create the directory. This function does it recursivily!
295                 make_writable_directory($dir);
296             }
297             return $dir . '/' . $key . '.cache';
298         }
299     }
301     /**
302      * Retrieves an item from the cache store given its key.
303      *
304      * @param string $key The key to retrieve
305      * @return mixed The data that was associated with the key, or false if the key did not exist.
306      */
307     public function get($key) {
308         $filename = $key.'.cache';
309         $file = $this->file_path_for_key($key);
310         $ttl = $this->definition->get_ttl();
311         if ($ttl) {
312             $maxtime = cache::now() - $ttl;
313         }
314         $readfile = false;
315         if ($this->prescan && array_key_exists($key, $this->keys)) {
316             if (!$ttl || $this->keys[$filename] >= $maxtime && file_exists($file)) {
317                 $readfile = true;
318             } else {
319                 $this->delete($key);
320             }
321         } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) {
322             $readfile = true;
323         }
324         if (!$readfile) {
325             return false;
326         }
327         // Check the filesize first, likely not needed but important none the less.
328         $filesize = filesize($file);
329         if (!$filesize) {
330             return false;
331         }
332         // Open ensuring the file for writing, truncating it and setting the pointer to the start.
333         if (!$handle = fopen($file, 'rb')) {
334             return false;
335         }
336         // Lock it up!
337         // We don't care if this succeeds or not, on some systems it will, on some it won't, meah either way.
338         flock($handle, LOCK_SH);
339         // HACK ALERT
340         // There is a problem when reading from the file during PHPUNIT tests. For one reason or another the filesize is not correct
341         // Doesn't happen during normal operation, just during unit tests.
342         // Read it.
343         $data = fread($handle, $filesize+128);
344         // Unlock it.
345         flock($handle, LOCK_UN);
346         // Return it unserialised.
347         return $this->prep_data_after_read($data);
348     }
350     /**
351      * Retrieves several items from the cache store in a single transaction.
352      *
353      * If not all of the items are available in the cache then the data value for those that are missing will be set to false.
354      *
355      * @param array $keys The array of keys to retrieve
356      * @return array An array of items from the cache. There will be an item for each key, those that were not in the store will
357      *      be set to false.
358      */
359     public function get_many($keys) {
360         $result = array();
361         foreach ($keys as $key) {
362             $result[$key] = $this->get($key);
363         }
364         return $result;
365     }
367     /**
368      * Deletes an item from the cache store.
369      *
370      * @param string $key The key to delete.
371      * @return bool Returns true if the operation was a success, false otherwise.
372      */
373     public function delete($key) {
374         $filename = $key.'.cache';
375         $file = $this->file_path_for_key($key);
377         if (@unlink($file)) {
378             unset($this->keys[$filename]);
379             return true;
380         }
382         return false;
383     }
385     /**
386      * Deletes several keys from the cache in a single action.
387      *
388      * @param array $keys The keys to delete
389      * @return int The number of items successfully deleted.
390      */
391     public function delete_many(array $keys) {
392         $count = 0;
393         foreach ($keys as $key) {
394             if ($this->delete($key)) {
395                 $count++;
396             }
397         }
398         return $count;
399     }
401     /**
402      * Sets an item in the cache given its key and data value.
403      *
404      * @param string $key The key to use.
405      * @param mixed $data The data to set.
406      * @return bool True if the operation was a success false otherwise.
407      */
408     public function set($key, $data) {
409         $this->ensure_path_exists();
410         $filename = $key.'.cache';
411         $file = $this->file_path_for_key($key, true);
412         $result = $this->write_file($file, $this->prep_data_before_save($data));
413         if (!$result) {
414             // Couldn't write the file.
415             return false;
416         }
417         // Record the key if required.
418         if ($this->prescan) {
419             $this->keys[$filename] = cache::now() + 1;
420         }
421         // Return true.. it all worked **miracles**.
422         return true;
423     }
425     /**
426      * Prepares data to be stored in a file.
427      *
428      * @param mixed $data
429      * @return string
430      */
431     protected function prep_data_before_save($data) {
432         return serialize($data);
433     }
435     /**
436      * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save.
437      *
438      * @param string $data
439      * @return mixed
440      * @throws coding_exception
441      */
442     protected function prep_data_after_read($data) {
443         $result = @unserialize($data);
444         if ($result === false) {
445             throw new coding_exception('Failed to unserialise data from file. Either failed to read, or failed to write.');
446         }
447         return $result;
448     }
450     /**
451      * Sets many items in the cache in a single transaction.
452      *
453      * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
454      *      keys, 'key' and 'value'.
455      * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
456      *      sent ... if they care that is.
457      */
458     public function set_many(array $keyvaluearray) {
459         $count = 0;
460         foreach ($keyvaluearray as $pair) {
461             if ($this->set($pair['key'], $pair['value'])) {
462                 $count++;
463             }
464         }
465         return $count;
466     }
468     /**
469      * Checks if the store has a record for the given key and returns true if so.
470      *
471      * @param string $key
472      * @return bool
473      */
474     public function has($key) {
475         $filename = $key.'.cache';
476         $maxtime = cache::now() - $this->definition->get_ttl();
477         if ($this->prescan) {
478             return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime;
479         }
480         $file = $this->file_path_for_key($key);
481         return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime));
482     }
484     /**
485      * Returns true if the store contains records for all of the given keys.
486      *
487      * @param array $keys
488      * @return bool
489      */
490     public function has_all(array $keys) {
491         foreach ($keys as $key) {
492             if (!$this->has($key)) {
493                 return false;
494             }
495         }
496         return true;
497     }
499     /**
500      * Returns true if the store contains records for any of the given keys.
501      *
502      * @param array $keys
503      * @return bool
504      */
505     public function has_any(array $keys) {
506         foreach ($keys as $key) {
507             if ($this->has($key)) {
508                 return true;
509             }
510         }
511         return false;
512     }
514     /**
515      * Purges the cache deleting all items within it.
516      *
517      * @return boolean True on success. False otherwise.
518      */
519     public function purge() {
520         $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
521         if (is_array($files)) {
522             foreach ($files as $filename) {
523                 @unlink($filename);
524             }
525         }
526         $this->keys = array();
527         return true;
528     }
530     /**
531      * Given the data from the add instance form this function creates a configuration array.
532      *
533      * @param stdClass $data
534      * @return array
535      */
536     public static function config_get_configuration_array($data) {
537         $config = array();
539         if (isset($data->path)) {
540             $config['path'] = $data->path;
541         }
542         if (isset($data->autocreate)) {
543             $config['autocreate'] = $data->autocreate;
544         }
545         if (isset($data->singledirectory)) {
546             $config['singledirectory'] = $data->singledirectory;
547         }
548         if (isset($data->prescan)) {
549             $config['prescan'] = $data->prescan;
550         }
552         return $config;
553     }
555     /**
556      * Allows the cache store to set its data against the edit form before it is shown to the user.
557      *
558      * @param moodleform $editform
559      * @param array $config
560      */
561     public static function config_set_edit_form_data(moodleform $editform, array $config) {
562         $data = array();
563         if (!empty($config['path'])) {
564             $data['path'] = $config['path'];
565         }
566         if (isset($config['autocreate'])) {
567             $data['autocreate'] = (bool)$config['autocreate'];
568         }
569         if (isset($config['singledirectory'])) {
570             $data['singledirectory'] = (bool)$config['singledirectory'];
571         }
572         if (isset($config['prescan'])) {
573             $data['prescan'] = (bool)$config['prescan'];
574         }
575         $editform->set_data($data);
576     }
578     /**
579      * Checks to make sure that the path for the file cache exists.
580      *
581      * @return bool
582      * @throws coding_exception
583      */
584     protected function ensure_path_exists() {
585         if (!is_writable($this->path)) {
586             if ($this->custompath && !$this->autocreate) {
587                 throw new coding_exception('File store path does not exist. It must exist and be writable by the web server.');
588             }
589             if (!make_writable_directory($this->path, false)) {
590                 throw new coding_exception('File store path does not exist and can not be created.');
591             }
592         }
593         return true;
594     }
596     /**
597      * Returns true if the user can add an instance of the store plugin.
598      *
599      * @return bool
600      */
601     public static function can_add_instance() {
602         return true;
603     }
605     /**
606      * Performs any necessary clean up when the store instance is being deleted.
607      *
608      * 1. Purges the cache directory.
609      * 2. Deletes the directory we created for this cache instances data.
610      */
611     public function cleanup() {
612         $this->purge();
613         @rmdir($this->path);
614     }
616     /**
617      * Generates an instance of the cache store that can be used for testing.
618      *
619      * Returns an instance of the cache store, or false if one cannot be created.
620      *
621      * @param cache_definition $definition
622      * @return cachestore_file
623      */
624     public static function initialise_test_instance(cache_definition $definition) {
625         $name = 'File test';
626         $path = make_cache_directory('cachestore_file_test');
627         $cache = new cachestore_file($name, array('path' => $path));
628         $cache->initialise($definition);
629         return $cache;
630     }
632     /**
633      * Writes your madness to a file.
634      *
635      * There are several things going on in this function to try to ensure what we don't end up with partial writes etc.
636      *   1. Files for writing are opened with the mode xb, the file must be created and can not already exist.
637      *   2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed.
638      *
639      * @param string $file Absolute file path
640      * @param string $content The content to write.
641      * @return bool
642      */
643     protected function write_file($file, $content) {
644         // Generate a temp file that is going to be unique. We'll rename it at the end to the desired file name.
645         // in this way we avoid partial writes.
646         $path = dirname($file);
647         while (true) {
648             $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp';
649             if (!file_exists($tempfile)) {
650                 break;
651             }
652         }
654         // Open the file with mode=x. This acts to create and open the file for writing only.
655         // If the file already exists this will return false.
656         // We also force binary.
657         $handle = @fopen($tempfile, 'xb+');
658         if ($handle === false) {
659             // File already exists... lock already exists, return false.
660             return false;
661         }
662         fwrite($handle, $content);
663         fflush($handle);
664         // Close the handle, we're done.
665         fclose($handle);
667         if (md5_file($tempfile) !== md5($content)) {
668             // The md5 of the content of the file must match the md5 of the content given to be written.
669             @unlink($tempfile);
670             return false;
671         }
673         // Finally rename the temp file to the desired file, returning the true|false result.
674         $result = rename($tempfile, $file);
675         if (!$result) {
676             // Failed to rename, don't leave files lying around.
677             @unlink($tempfile);
678         }
679         return $result;
680     }
682     /**
683      * Returns the name of this instance.
684      * @return string
685      */
686     public function my_name() {
687         return $this->name;
688     }