MDL-36825 cachestore_file: fixed isready condition check
[moodle.git] / cache / stores / file / lib.php
CommitLineData
62704f33
SH
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/>.
16
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 *
6fec1820 23 * @package cachestore_file
62704f33
SH
24 * @category cache
25 * @copyright 2012 Sam Hemelryk
26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 */
28
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 */
2b274ad0 40class cachestore_file extends cache_store implements cache_is_key_aware, cache_is_configurable {
62704f33
SH
41
42 /**
43 * The name of the store.
44 * @var string
45 */
46 protected $name;
47
48 /**
49 * The path to use for the file storage.
50 * @var string
51 */
52 protected $path = null;
53
54 /**
55 * Set to true when a prescan has been performed.
56 * @var bool
57 */
58 protected $prescan = false;
59
08aaa637
SH
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;
67
62704f33
SH
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;
73
74 /**
75 * Set to true if a custom path is being used.
76 * @var bool
77 */
78 protected $custompath = false;
79
80 /**
81 * An array of keys we are sure about presently.
82 * @var array
83 */
84 protected $keys = array();
85
86 /**
87 * True when the store is ready to be initialised.
88 * @var bool
89 */
90 protected $isready = false;
91
62704f33
SH
92 /**
93 * The cache definition this instance has been initialised with.
94 * @var cache_definition
95 */
96 protected $definition;
97
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;
e4f71b7c 126 debugging('The file cache store path is not writable for `'.$name.'`', DEBUG_DEVELOPER);
62704f33
SH
127 }
128 } else {
6fec1820 129 $path = make_cache_directory('cachestore_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name));
62704f33
SH
130 }
131 $this->isready = $path !== false;
132 $this->path = $path;
08aaa637
SH
133 // Check if we should prescan the directory.
134 if (array_key_exists('prescan', $configuration)) {
702651c7 135 $this->prescan = (bool)$configuration['prescan'];
08aaa637
SH
136 } else {
137 // Default is no, we should not prescan.
702651c7 138 $this->prescan = false;
08aaa637
SH
139 }
140 // Check if we should be storing in a single directory.
141 if (array_key_exists('singledirectory', $configuration)) {
702651c7 142 $this->singledirectory = (bool)$configuration['singledirectory'];
08aaa637
SH
143 } else {
144 // Default: No, we will use multiple directories.
702651c7 145 $this->singledirectory = false;
08aaa637 146 }
62704f33
SH
147 }
148
149 /**
150 * Returns true if this store instance is ready to be used.
151 * @return bool
152 */
153 public function is_ready() {
e4f71b7c 154 return $this->isready;
62704f33
SH
155 }
156
157 /**
158 * Returns true once this instance has been initialised.
159 *
160 * @return bool
161 */
162 public function is_initialised() {
163 return true;
164 }
165
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 }
177
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 }
187
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 }
196
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 }
206
62704f33
SH
207 /**
208 * Initialises the cache.
209 *
210 * Once this has been done the cache is all set to be used.
211 *
212 * @param cache_definition $definition
213 */
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;
221 }
222 if ($this->prescan) {
08aaa637
SH
223 $this->prescan_keys();
224 }
225 }
226
227 /**
228 * Pre-scan the cache to see which keys are present.
229 */
230 protected function prescan_keys() {
758dbdf8
SH
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);
235 }
08aaa637
SH
236 }
237 }
238
239 /**
240 * Gets a pattern suitable for use with glob to find all keys in the cache.
241 * @return string The pattern.
242 */
243 protected function glob_keys_pattern() {
244 if ($this->singledirectory) {
245 return $this->path . '/*.cache';
246 } else {
702651c7 247 return $this->path . '/*/*.cache';
08aaa637
SH
248 }
249 }
250
251 /**
252 * Returns the file path to use for the given key.
253 *
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.
257 */
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';
262 } else {
702651c7
SH
263 // We are using a single subdirectory to achieve 1 level.
264 $subdir = substr($key, 0, 3);
265 $dir = $this->path . '/' . $subdir;
08aaa637
SH
266 if ($create) {
267 // Create the directory. This function does it recursivily!
268 make_writable_directory($dir);
62704f33 269 }
08aaa637 270 return $dir . '/' . $key . '.cache';
62704f33
SH
271 }
272 }
273
274 /**
275 * Retrieves an item from the cache store given its key.
276 *
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.
279 */
280 public function get($key) {
281 $filename = $key.'.cache';
08aaa637 282 $file = $this->file_path_for_key($key);
62704f33
SH
283 $ttl = $this->definition->get_ttl();
284 if ($ttl) {
285 $maxtime = cache::now() - $ttl;
286 }
287 $readfile = false;
288 if ($this->prescan && array_key_exists($key, $this->keys)) {
289 if (!$ttl || $this->keys[$filename] >= $maxtime && file_exists($file)) {
290 $readfile = true;
291 } else {
292 $this->delete($key);
293 }
294 } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) {
295 $readfile = true;
296 }
297 if (!$readfile) {
298 return false;
299 }
170f821b 300 // Check the filesize first, likely not needed but important none the less.
62704f33
SH
301 $filesize = filesize($file);
302 if (!$filesize) {
303 return false;
304 }
305 // Open ensuring the file for writing, truncating it and setting the pointer to the start.
306 if (!$handle = fopen($file, 'rb')) {
307 return false;
308 }
309 // Lock it up!
170f821b 310 // We don't care if this succeeds or not, on some systems it will, on some it won't, meah either way.
62704f33
SH
311 flock($handle, LOCK_SH);
312 // HACK ALERT
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.
170f821b 315 // Read it.
62704f33 316 $data = fread($handle, $filesize+128);
170f821b 317 // Unlock it.
62704f33
SH
318 flock($handle, LOCK_UN);
319 // Return it unserialised.
320 return $this->prep_data_after_read($data);
321 }
322
323 /**
324 * Retrieves several items from the cache store in a single transaction.
325 *
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.
327 *
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
330 * be set to false.
331 */
332 public function get_many($keys) {
333 $result = array();
334 foreach ($keys as $key) {
335 $result[$key] = $this->get($key);
336 }
337 return $result;
338 }
170f821b 339
62704f33
SH
340 /**
341 * Deletes an item from the cache store.
342 *
343 * @param string $key The key to delete.
344 * @return bool Returns true if the operation was a success, false otherwise.
345 */
346 public function delete($key) {
347 $filename = $key.'.cache';
08aaa637 348 $file = $this->file_path_for_key($key);
f86ce2dc
MS
349
350 if (@unlink($file)) {
351 unset($this->keys[$filename]);
352 return true;
353 }
354
355 return false;
62704f33
SH
356 }
357
358 /**
359 * Deletes several keys from the cache in a single action.
360 *
361 * @param array $keys The keys to delete
362 * @return int The number of items successfully deleted.
363 */
364 public function delete_many(array $keys) {
365 $count = 0;
366 foreach ($keys as $key) {
367 if ($this->delete($key)) {
368 $count++;
369 }
370 }
371 return $count;
372 }
373
374 /**
375 * Sets an item in the cache given its key and data value.
376 *
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.
380 */
381 public function set($key, $data) {
382 $this->ensure_path_exists();
383 $filename = $key.'.cache';
08aaa637 384 $file = $this->file_path_for_key($key, true);
62704f33
SH
385 $result = $this->write_file($file, $this->prep_data_before_save($data));
386 if (!$result) {
387 // Couldn't write the file.
388 return false;
389 }
170f821b 390 // Record the key if required.
62704f33
SH
391 if ($this->prescan) {
392 $this->keys[$filename] = cache::now() + 1;
393 }
170f821b 394 // Return true.. it all worked **miracles**.
62704f33
SH
395 return true;
396 }
397
398 /**
399 * Prepares data to be stored in a file.
400 *
401 * @param mixed $data
402 * @return string
403 */
404 protected function prep_data_before_save($data) {
405 return serialize($data);
406 }
407
408 /**
409 * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save.
410 *
411 * @param string $data
412 * @return mixed
413 * @throws coding_exception
414 */
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.');
419 }
420 return $result;
421 }
422
423 /**
424 * Sets many items in the cache in a single transaction.
425 *
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.
430 */
431 public function set_many(array $keyvaluearray) {
432 $count = 0;
433 foreach ($keyvaluearray as $pair) {
434 if ($this->set($pair['key'], $pair['value'])) {
435 $count++;
436 }
437 }
438 return $count;
439 }
440
441 /**
442 * Checks if the store has a record for the given key and returns true if so.
443 *
444 * @param string $key
445 * @return bool
446 */
447 public function has($key) {
448 $filename = $key.'.cache';
62704f33
SH
449 $maxtime = cache::now() - $this->definition->get_ttl();
450 if ($this->prescan) {
451 return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime;
452 }
08aaa637 453 $file = $this->file_path_for_key($key);
62704f33
SH
454 return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime));
455 }
456
457 /**
458 * Returns true if the store contains records for all of the given keys.
459 *
460 * @param array $keys
461 * @return bool
462 */
463 public function has_all(array $keys) {
464 foreach ($keys as $key) {
465 if (!$this->has($key)) {
466 return false;
467 }
468 }
469 return true;
470 }
471
472 /**
473 * Returns true if the store contains records for any of the given keys.
474 *
475 * @param array $keys
476 * @return bool
477 */
478 public function has_any(array $keys) {
479 foreach ($keys as $key) {
480 if ($this->has($key)) {
481 return true;
482 }
483 }
484 return false;
485 }
170f821b 486
62704f33
SH
487 /**
488 * Purges the cache deleting all items within it.
489 *
490 * @return boolean True on success. False otherwise.
491 */
492 public function purge() {
758dbdf8
SH
493 $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
494 if (is_array($files)) {
495 foreach ($files as $filename) {
496 @unlink($filename);
497 }
62704f33
SH
498 }
499 $this->keys = array();
500 return true;
501 }
502
d4d2f27c
MS
503 /**
504 * Given the data from the add instance form this function creates a configuration array.
505 *
506 * @param stdClass $data
507 * @return array
508 */
509 public static function config_get_configuration_array($data) {
510 $config = array();
511
d4d2f27c
MS
512 if (isset($data->path)) {
513 $config['path'] = $data->path;
514 }
515 if (isset($data->autocreate)) {
516 $config['autocreate'] = $data->autocreate;
517 }
49c497ff
SH
518 if (isset($data->singledirectory)) {
519 $config['singledirectory'] = $data->singledirectory;
520 }
d4d2f27c
MS
521 if (isset($data->prescan)) {
522 $config['prescan'] = $data->prescan;
523 }
524
525 return $config;
526 }
527
81ede547
SH
528 /**
529 * Allows the cache store to set its data against the edit form before it is shown to the user.
530 *
531 * @param moodleform $editform
532 * @param array $config
533 */
534 public static function config_set_edit_form_data(moodleform $editform, array $config) {
535 $data = array();
536 if (!empty($config['path'])) {
537 $data['path'] = $config['path'];
538 }
d837df0d
SH
539 if (isset($config['autocreate'])) {
540 $data['autocreate'] = (bool)$config['autocreate'];
81ede547 541 }
d837df0d
SH
542 if (isset($config['singledirectory'])) {
543 $data['singledirectory'] = (bool)$config['singledirectory'];
544 }
545 if (isset($config['prescan'])) {
546 $data['prescan'] = (bool)$config['prescan'];
81ede547
SH
547 }
548 $editform->set_data($data);
549 }
550
62704f33
SH
551 /**
552 * Checks to make sure that the path for the file cache exists.
553 *
554 * @return bool
555 * @throws coding_exception
556 */
557 protected function ensure_path_exists() {
558 if (!is_writable($this->path)) {
559 if ($this->custompath && !$this->autocreate) {
170f821b 560 throw new coding_exception('File store path does not exist. It must exist and be writable by the web server.');
62704f33
SH
561 }
562 if (!make_writable_directory($this->path, false)) {
563 throw new coding_exception('File store path does not exist and can not be created.');
564 }
565 }
566 return true;
567 }
568
62704f33
SH
569 /**
570 * Performs any necessary clean up when the store instance is being deleted.
571 *
572 * 1. Purges the cache directory.
573 * 2. Deletes the directory we created for this cache instances data.
574 */
575 public function cleanup() {
576 $this->purge();
577 @rmdir($this->path);
578 }
579
580 /**
581 * Generates an instance of the cache store that can be used for testing.
582 *
583 * Returns an instance of the cache store, or false if one cannot be created.
584 *
585 * @param cache_definition $definition
6fec1820 586 * @return cachestore_file
62704f33
SH
587 */
588 public static function initialise_test_instance(cache_definition $definition) {
589 $name = 'File test';
6fec1820
SH
590 $path = make_cache_directory('cachestore_file_test');
591 $cache = new cachestore_file($name, array('path' => $path));
62704f33
SH
592 $cache->initialise($definition);
593 return $cache;
594 }
595
596 /**
597 * Writes your madness to a file.
598 *
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.
34c84c72 601 * 2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed.
62704f33
SH
602 *
603 * @param string $file Absolute file path
604 * @param string $content The content to write.
605 * @return bool
606 */
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);
611 while (true) {
612 $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp';
613 if (!file_exists($tempfile)) {
614 break;
615 }
616 }
617
62704f33
SH
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.
624 return false;
625 }
62704f33
SH
626 fwrite($handle, $content);
627 fflush($handle);
628 // Close the handle, we're done.
629 fclose($handle);
630
62704f33
SH
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.
633 @unlink($tempfile);
634 return false;
635 }
636
637 // Finally rename the temp file to the desired file, returning the true|false result.
638 $result = rename($tempfile, $file);
639 if (!$result) {
640 // Failed to rename, don't leave files lying around.
641 @unlink($tempfile);
642 }
643 return $result;
644 }
34c84c72
SH
645
646 /**
647 * Returns the name of this instance.
648 * @return string
649 */
650 public function my_name() {
651 return $this->name;
652 }
bc8f1957 653}