2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * The library file for the file cache store.
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.
23 * @package cachestore_file
25 * @copyright 2012 Sam Hemelryk
26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30 * The file store class.
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
37 * @copyright 2012 Sam Hemelryk
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 class cachestore_file extends cache_store_base implements cache_is_key_aware {
43 * The name of the store.
49 * The path to use for the file storage.
52 protected $path = null;
55 * Set to true when a prescan has been performed.
58 protected $prescan = false;
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.
66 protected $singledirectory = false;
69 * Set to true when the path should be automatically created if it does not yet exist.
72 protected $autocreate = false;
75 * Set to true if a custom path is being used.
78 protected $custompath = false;
81 * An array of keys we are sure about presently.
84 protected $keys = array();
87 * True when the store is ready to be initialised.
90 protected $isready = false;
93 * The cache definition this instance has been initialised with.
94 * @var cache_definition
96 protected $definition;
99 * Constructs the store instance.
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.
104 * @param string $name
105 * @param array $configuration
107 public function __construct($name, array $configuration = array()) {
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)) {
117 debugging('Error trying to autocreate file store path. '.$path, DEBUG_DEVELOPER);
121 debugging('The given file cache store path does not exist. '.$path, DEBUG_DEVELOPER);
124 if ($path !== false && !is_writable($path)) {
126 debugging('The given file cache store path is not writable. '.$path, DEBUG_DEVELOPER);
129 $path = make_cache_directory('cachestore_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name));
131 $this->isready = $path !== false;
133 // Check if we should prescan the directory.
134 if (array_key_exists('prescan', $configuration)) {
135 $this->prescan = (bool)$configuration['prescan'];
137 // Default is no, we should not prescan.
138 $this->prescan = false;
140 // Check if we should be storing in a single directory.
141 if (array_key_exists('singledirectory', $configuration)) {
142 $this->singledirectory = (bool)$configuration['singledirectory'];
144 // Default: No, we will use multiple directories.
145 $this->singledirectory = false;
150 * Returns true if this store instance is ready to be used.
153 public function is_ready() {
154 return ($this->path !== null);
158 * Returns true once this instance has been initialised.
162 public function is_initialised() {
167 * Returns the supported features as a combined int.
169 * @param array $configuration
172 public static function get_supported_features(array $configuration = array()) {
173 $supported = self::SUPPORTS_DATA_GUARANTEE +
174 self::SUPPORTS_NATIVE_TTL;
179 * Returns the supported modes as a combined int.
181 * @param array $configuration
184 public static function get_supported_modes(array $configuration = array()) {
185 return self::MODE_APPLICATION + self::MODE_SESSION;
189 * Returns true if the store requirements are met.
193 public static function are_requirements_met() {
198 * Returns true if the given mode is supported by this store.
200 * @param int $mode One of cache_store::MODE_*
203 public static function is_supported_mode($mode) {
204 return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
208 * Initialises the cache.
210 * Once this has been done the cache is all set to be used.
212 * @param cache_definition $definition
214 public function initialise(cache_definition $definition) {
215 $this->definition = $definition;
216 $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
217 $this->path .= '/'.$hash;
218 make_writable_directory($this->path);
219 if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
220 $this->prescan = false;
222 if ($this->prescan) {
223 $this->prescan_keys();
228 * Pre-scan the cache to see which keys are present.
230 protected function prescan_keys() {
231 $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
232 if (is_array($files)) {
233 foreach ($files as $filename) {
234 $this->keys[basename($filename)] = filemtime($filename);
240 * Gets a pattern suitable for use with glob to find all keys in the cache.
241 * @return string The pattern.
243 protected function glob_keys_pattern() {
244 if ($this->singledirectory) {
245 return $this->path . '/*.cache';
247 return $this->path . '/*/*.cache';
252 * Returns the file path to use for the given key.
254 * @param string $key The key to generate a file path for.
255 * @param bool $create If set to the true the directory structure the key requires will be created.
256 * @return string The full path to the file that stores a particular cache key.
258 protected function file_path_for_key($key, $create = false) {
259 if ($this->singledirectory) {
260 // Its a single directory, easy, just the store instances path + the file name.
261 return $this->path . '/' . $key . '.cache';
263 // We are using a single subdirectory to achieve 1 level.
264 $subdir = substr($key, 0, 3);
265 $dir = $this->path . '/' . $subdir;
267 // Create the directory. This function does it recursivily!
268 make_writable_directory($dir);
270 return $dir . '/' . $key . '.cache';
275 * Retrieves an item from the cache store given its key.
277 * @param string $key The key to retrieve
278 * @return mixed The data that was associated with the key, or false if the key did not exist.
280 public function get($key) {
281 $filename = $key.'.cache';
282 $file = $this->file_path_for_key($key);
283 $ttl = $this->definition->get_ttl();
285 $maxtime = cache::now() - $ttl;
288 if ($this->prescan && array_key_exists($key, $this->keys)) {
289 if (!$ttl || $this->keys[$filename] >= $maxtime && file_exists($file)) {
294 } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) {
300 // Check the filesize first, likely not needed but important none the less.
301 $filesize = filesize($file);
305 // Open ensuring the file for writing, truncating it and setting the pointer to the start.
306 if (!$handle = fopen($file, 'rb')) {
310 // We don't care if this succeeds or not, on some systems it will, on some it won't, meah either way.
311 flock($handle, LOCK_SH);
313 // There is a problem when reading from the file during PHPUNIT tests. For one reason or another the filesize is not correct
314 // Doesn't happen during normal operation, just during unit tests.
316 $data = fread($handle, $filesize+128);
318 flock($handle, LOCK_UN);
319 // Return it unserialised.
320 return $this->prep_data_after_read($data);
324 * Retrieves several items from the cache store in a single transaction.
326 * 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.
328 * @param array $keys The array of keys to retrieve
329 * @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
332 public function get_many($keys) {
334 foreach ($keys as $key) {
335 $result[$key] = $this->get($key);
341 * Deletes an item from the cache store.
343 * @param string $key The key to delete.
344 * @return bool Returns true if the operation was a success, false otherwise.
346 public function delete($key) {
347 $filename = $key.'.cache';
348 $file = $this->file_path_for_key($key);
350 if (@unlink($file)) {
351 unset($this->keys[$filename]);
359 * Deletes several keys from the cache in a single action.
361 * @param array $keys The keys to delete
362 * @return int The number of items successfully deleted.
364 public function delete_many(array $keys) {
366 foreach ($keys as $key) {
367 if ($this->delete($key)) {
375 * Sets an item in the cache given its key and data value.
377 * @param string $key The key to use.
378 * @param mixed $data The data to set.
379 * @return bool True if the operation was a success false otherwise.
381 public function set($key, $data) {
382 $this->ensure_path_exists();
383 $filename = $key.'.cache';
384 $file = $this->file_path_for_key($key, true);
385 $result = $this->write_file($file, $this->prep_data_before_save($data));
387 // Couldn't write the file.
390 // Record the key if required.
391 if ($this->prescan) {
392 $this->keys[$filename] = cache::now() + 1;
394 // Return true.. it all worked **miracles**.
399 * Prepares data to be stored in a file.
404 protected function prep_data_before_save($data) {
405 return serialize($data);
409 * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save.
411 * @param string $data
413 * @throws coding_exception
415 protected function prep_data_after_read($data) {
416 $result = @unserialize($data);
417 if ($result === false) {
418 throw new coding_exception('Failed to unserialise data from file. Either failed to read, or failed to write.');
424 * Sets many items in the cache in a single transaction.
426 * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
427 * keys, 'key' and 'value'.
428 * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
429 * sent ... if they care that is.
431 public function set_many(array $keyvaluearray) {
433 foreach ($keyvaluearray as $pair) {
434 if ($this->set($pair['key'], $pair['value'])) {
442 * Checks if the store has a record for the given key and returns true if so.
447 public function has($key) {
448 $filename = $key.'.cache';
449 $maxtime = cache::now() - $this->definition->get_ttl();
450 if ($this->prescan) {
451 return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime;
453 $file = $this->file_path_for_key($key);
454 return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime));
458 * Returns true if the store contains records for all of the given keys.
463 public function has_all(array $keys) {
464 foreach ($keys as $key) {
465 if (!$this->has($key)) {
473 * Returns true if the store contains records for any of the given keys.
478 public function has_any(array $keys) {
479 foreach ($keys as $key) {
480 if ($this->has($key)) {
488 * Purges the cache deleting all items within it.
490 * @return boolean True on success. False otherwise.
492 public function purge() {
493 $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
494 if (is_array($files)) {
495 foreach ($files as $filename) {
499 $this->keys = array();
504 * Given the data from the add instance form this function creates a configuration array.
506 * @param stdClass $data
509 public static function config_get_configuration_array($data) {
512 if (isset($data->path)) {
513 $config['path'] = $data->path;
515 if (isset($data->autocreate)) {
516 $config['autocreate'] = $data->autocreate;
518 if (isset($data->singledirectory)) {
519 $config['singledirectory'] = $data->singledirectory;
521 if (isset($data->prescan)) {
522 $config['prescan'] = $data->prescan;
529 * Allows the cache store to set its data against the edit form before it is shown to the user.
531 * @param moodleform $editform
532 * @param array $config
534 public static function config_set_edit_form_data(moodleform $editform, array $config) {
536 if (!empty($config['path'])) {
537 $data['path'] = $config['path'];
539 if (isset($config['autocreate'])) {
540 $data['autocreate'] = (bool)$config['autocreate'];
542 if (isset($config['singledirectory'])) {
543 $data['singledirectory'] = (bool)$config['singledirectory'];
545 if (isset($config['prescan'])) {
546 $data['prescan'] = (bool)$config['prescan'];
548 $editform->set_data($data);
552 * Checks to make sure that the path for the file cache exists.
555 * @throws coding_exception
557 protected function ensure_path_exists() {
558 if (!is_writable($this->path)) {
559 if ($this->custompath && !$this->autocreate) {
560 throw new coding_exception('File store path does not exist. It must exist and be writable by the web server.');
562 if (!make_writable_directory($this->path, false)) {
563 throw new coding_exception('File store path does not exist and can not be created.');
570 * Performs any necessary clean up when the store instance is being deleted.
572 * 1. Purges the cache directory.
573 * 2. Deletes the directory we created for this cache instances data.
575 public function cleanup() {
581 * Generates an instance of the cache store that can be used for testing.
583 * Returns an instance of the cache store, or false if one cannot be created.
585 * @param cache_definition $definition
586 * @return cachestore_file
588 public static function initialise_test_instance(cache_definition $definition) {
590 $path = make_cache_directory('cachestore_file_test');
591 $cache = new cachestore_file($name, array('path' => $path));
592 $cache->initialise($definition);
597 * Writes your madness to a file.
599 * There are several things going on in this function to try to ensure what we don't end up with partial writes etc.
600 * 1. Files for writing are opened with the mode xb, the file must be created and can not already exist.
601 * 2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed.
603 * @param string $file Absolute file path
604 * @param string $content The content to write.
607 protected function write_file($file, $content) {
608 // Generate a temp file that is going to be unique. We'll rename it at the end to the desired file name.
609 // in this way we avoid partial writes.
610 $path = dirname($file);
612 $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp';
613 if (!file_exists($tempfile)) {
618 // Open the file with mode=x. This acts to create and open the file for writing only.
619 // If the file already exists this will return false.
620 // We also force binary.
621 $handle = @fopen($tempfile, 'xb+');
622 if ($handle === false) {
623 // File already exists... lock already exists, return false.
626 fwrite($handle, $content);
628 // Close the handle, we're done.
631 if (md5_file($tempfile) !== md5($content)) {
632 // The md5 of the content of the file must match the md5 of the content given to be written.
637 // Finally rename the temp file to the desired file, returning the true|false result.
638 $result = rename($tempfile, $file);
640 // Failed to rename, don't leave files lying around.
647 * Returns the name of this instance.
650 public function my_name() {