MDL-25290 cache_file: Added default file cache store
[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 *
23 * @package cache_file
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 */
40class cache_store_file implements cache_store, cache_is_lockable, cache_is_key_aware {
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
60 /**
61 * Set to true when the path should be automatically created if it does not yet exist.
62 * @var bool
63 */
64 protected $autocreate = false;
65
66 /**
67 * Set to true if a custom path is being used.
68 * @var bool
69 */
70 protected $custompath = false;
71
72 /**
73 * An array of keys we are sure about presently.
74 * @var array
75 */
76 protected $keys = array();
77
78 /**
79 * True when the store is ready to be initialised.
80 * @var bool
81 */
82 protected $isready = false;
83
84 /**
85 * An array containing the locks this store instance owns presently.
86 * @var array
87 */
88 protected $locks = array();
89
90 /**
91 * The cache definition this instance has been initialised with.
92 * @var cache_definition
93 */
94 protected $definition;
95
96 /**
97 * Constructs the store instance.
98 *
99 * Noting that this function is not an initialisation. It is used to prepare the store for use.
100 * The store will be initialised when required and will be provided with a cache_definition at that time.
101 *
102 * @param string $name
103 * @param array $configuration
104 */
105 public function __construct($name, array $configuration = array()) {
106 $this->name = $name;
107 if (array_key_exists('path', $configuration) && $configuration['path'] !== '') {
108 $this->custompath = true;
109 $this->autocreate = !empty($configuration['autocreate']);
110 $path = (string)$configuration['path'];
111 if (!is_dir($path)) {
112 if ($this->autocreate) {
113 if (!make_writable_directory($path, false)) {
114 $path = false;
115 debugging('Error trying to autocreate file store path. '.$path, DEBUG_DEVELOPER);
116 }
117 } else {
118 $path = false;
119 debugging('The given file cache store path does not exist. '.$path, DEBUG_DEVELOPER);
120 }
121 }
122 if ($path !== false && !is_writable($path)) {
123 $path = false;
124 debugging('The given file cache store path is not writable. '.$path, DEBUG_DEVELOPER);
125 }
126 } else {
127 $path = make_cache_directory('cache_store_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name));
128 }
129 $this->isready = $path !== false;
130 $this->path = $path;
131 $this->prescan = array_key_exists('prescan', $configuration) ? (bool)$configuration['prescan'] : false;
132 }
133
134 /**
135 * Returns true if this store instance is ready to be used.
136 * @return bool
137 */
138 public function is_ready() {
139 return ($this->path !== null);
140 }
141
142 /**
143 * Returns true once this instance has been initialised.
144 *
145 * @return bool
146 */
147 public function is_initialised() {
148 return true;
149 }
150
151 /**
152 * Returns the supported features as a combined int.
153 *
154 * @param array $configuration
155 * @return int
156 */
157 public static function get_supported_features(array $configuration = array()) {
158 $supported = self::SUPPORTS_DATA_GUARANTEE +
159 self::SUPPORTS_NATIVE_TTL;
160 return $supported;
161 }
162
163 /**
164 * Returns the supported modes as a combined int.
165 *
166 * @param array $configuration
167 * @return int
168 */
169 public static function get_supported_modes(array $configuration = array()) {
170 return self::MODE_APPLICATION + self::MODE_SESSION;
171 }
172
173 /**
174 * Returns true if the store requirements are met.
175 *
176 * @return bool
177 */
178 public static function are_requirements_met() {
179 return true;
180 }
181
182 /**
183 * Returns true if the given mode is supported by this store.
184 *
185 * @param int $mode One of cache_store::MODE_*
186 * @return bool
187 */
188 public static function is_supported_mode($mode) {
189 return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
190 }
191
192 /**
193 * Returns true if the store instance supports multiple identifiers.
194 *
195 * @return bool
196 */
197 public function supports_multiple_indentifiers() {
198 return false;
199 }
200
201 /**
202 * Returns true if the store instance guarantees data.
203 *
204 * @return bool
205 */
206 public function supports_data_guarantee() {
207 return true;
208 }
209
210 /**
211 * Returns true if the store instance supports native ttl.
212 *
213 * @return bool
214 */
215 public function supports_native_ttl() {
216 return true;
217 }
218
219 /**
220 * Initialises the cache.
221 *
222 * Once this has been done the cache is all set to be used.
223 *
224 * @param cache_definition $definition
225 */
226 public function initialise(cache_definition $definition) {
227 $this->definition = $definition;
228 $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
229 $this->path .= '/'.$hash;
230 make_writable_directory($this->path);
231 if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
232 $this->prescan = false;
233 }
234 if ($this->prescan) {
235 $pattern = $this->path.'/*.cache';
236 foreach (glob($pattern, GLOB_MARK | GLOB_NOSORT) as $filename) {
237 $this->keys[basename($filename)] = filemtime($filename);
238 }
239 }
240 }
241
242 /**
243 * Retrieves an item from the cache store given its key.
244 *
245 * @param string $key The key to retrieve
246 * @return mixed The data that was associated with the key, or false if the key did not exist.
247 */
248 public function get($key) {
249 $filename = $key.'.cache';
250 $file = $this->path.'/'.$filename;
251 $ttl = $this->definition->get_ttl();
252 if ($ttl) {
253 $maxtime = cache::now() - $ttl;
254 }
255 $readfile = false;
256 if ($this->prescan && array_key_exists($key, $this->keys)) {
257 if (!$ttl || $this->keys[$filename] >= $maxtime && file_exists($file)) {
258 $readfile = true;
259 } else {
260 $this->delete($key);
261 }
262 } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) {
263 $readfile = true;
264 }
265 if (!$readfile) {
266 return false;
267 }
268 // Check the filesize first, likely not needed but important none the less
269 $filesize = filesize($file);
270 if (!$filesize) {
271 return false;
272 }
273 // Open ensuring the file for writing, truncating it and setting the pointer to the start.
274 if (!$handle = fopen($file, 'rb')) {
275 return false;
276 }
277 // Lock it up!
278 // We don't care if this succeeds or not, on some systems it will, on some it won't, meah either way
279 flock($handle, LOCK_SH);
280 // HACK ALERT
281 // There is a problem when reading from the file during PHPUNIT tests. For one reason or another the filesize is not correct
282 // Doesn't happen during normal operation, just during unit tests.
283 // Read it
284 $data = fread($handle, $filesize+128);
285 // Unlock it
286 flock($handle, LOCK_UN);
287 // Return it unserialised.
288 return $this->prep_data_after_read($data);
289 }
290
291 /**
292 * Retrieves several items from the cache store in a single transaction.
293 *
294 * 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.
295 *
296 * @param array $keys The array of keys to retrieve
297 * @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
298 * be set to false.
299 */
300 public function get_many($keys) {
301 $result = array();
302 foreach ($keys as $key) {
303 $result[$key] = $this->get($key);
304 }
305 return $result;
306 }
307
308 /**
309 * Deletes an item from the cache store.
310 *
311 * @param string $key The key to delete.
312 * @return bool Returns true if the operation was a success, false otherwise.
313 */
314 public function delete($key) {
315 $filename = $key.'.cache';
316 $file = $this->path.'/'.$filename;
317 $result = @unlink($file);
318 unset($this->keys[$filename]);
319 return $result;
320 }
321
322 /**
323 * Deletes several keys from the cache in a single action.
324 *
325 * @param array $keys The keys to delete
326 * @return int The number of items successfully deleted.
327 */
328 public function delete_many(array $keys) {
329 $count = 0;
330 foreach ($keys as $key) {
331 if ($this->delete($key)) {
332 $count++;
333 }
334 }
335 return $count;
336 }
337
338 /**
339 * Sets an item in the cache given its key and data value.
340 *
341 * @param string $key The key to use.
342 * @param mixed $data The data to set.
343 * @return bool True if the operation was a success false otherwise.
344 */
345 public function set($key, $data) {
346 $this->ensure_path_exists();
347 $filename = $key.'.cache';
348 $file = $this->path.'/'.$filename;
349 $result = $this->write_file($file, $this->prep_data_before_save($data));
350 if (!$result) {
351 // Couldn't write the file.
352 return false;
353 }
354 // Record the key if required
355 if ($this->prescan) {
356 $this->keys[$filename] = cache::now() + 1;
357 }
358 // Return true.. it all worked **miracles**
359 return true;
360 }
361
362 /**
363 * Prepares data to be stored in a file.
364 *
365 * @param mixed $data
366 * @return string
367 */
368 protected function prep_data_before_save($data) {
369 return serialize($data);
370 }
371
372 /**
373 * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save.
374 *
375 * @param string $data
376 * @return mixed
377 * @throws coding_exception
378 */
379 protected function prep_data_after_read($data) {
380 $result = @unserialize($data);
381 if ($result === false) {
382 throw new coding_exception('Failed to unserialise data from file. Either failed to read, or failed to write.');
383 }
384 return $result;
385 }
386
387 /**
388 * Sets many items in the cache in a single transaction.
389 *
390 * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
391 * keys, 'key' and 'value'.
392 * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
393 * sent ... if they care that is.
394 */
395 public function set_many(array $keyvaluearray) {
396 $count = 0;
397 foreach ($keyvaluearray as $pair) {
398 if ($this->set($pair['key'], $pair['value'])) {
399 $count++;
400 }
401 }
402 return $count;
403 }
404
405 /**
406 * Checks if the store has a record for the given key and returns true if so.
407 *
408 * @param string $key
409 * @return bool
410 */
411 public function has($key) {
412 $filename = $key.'.cache';
413 $file = $this->path.'/'.$key.'.cache';
414 $maxtime = cache::now() - $this->definition->get_ttl();
415 if ($this->prescan) {
416 return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime;
417 }
418 return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime));
419 }
420
421 /**
422 * Returns true if the store contains records for all of the given keys.
423 *
424 * @param array $keys
425 * @return bool
426 */
427 public function has_all(array $keys) {
428 foreach ($keys as $key) {
429 if (!$this->has($key)) {
430 return false;
431 }
432 }
433 return true;
434 }
435
436 /**
437 * Returns true if the store contains records for any of the given keys.
438 *
439 * @param array $keys
440 * @return bool
441 */
442 public function has_any(array $keys) {
443 foreach ($keys as $key) {
444 if ($this->has($key)) {
445 return true;
446 }
447 }
448 return false;
449 }
450
451 /**
452 * Acquires a lock for the key with the given identifier.
453 *
454 * @param string $key The key to acquire a lock for.
455 * @param string $identifier The identifier who will own the lock.
456 * @return bool True if the lock could be acquired, false otherwise.
457 */
458 public function acquire_lock($key, $identifier) {
459 if (array_key_exists($key, $this->locks) && $this->locks[$key] == $identifier) {
460 // We already have the lock, return true.
461 return true;
462 }
463 $result = cache_lock::lock($key, false);
464 if ($result) {
465 $this->locks[$key] = $identifier;
466 }
467 return $result;
468 }
469
470 /**
471 * Releases the lock provided it belongs to the identifier.
472 *
473 * @param string $key The key to the lock is for.
474 * @param string $identifier The identifier of the caller.
475 * @return bool True if the lock has been released, false if there was a problem releasing the lock.
476 */
477 public function release_lock($key, $identifier) {
478 if (array_key_exists($key, $this->locks) && $this->locks[$key] == $identifier) {
479 $outcome = cache_lock::unlock($key);
480 return $outcome;
481 }
482 return false;
483 }
484
485 /**
486 * Returns true if the given key has a lock and it belongs to the identifier.
487 *
488 * @param string $key The key to the lock is for.
489 * @param string $identifier The identifier of the caller.
490 * @return bool True if this code has the lock, false if there is a lock but this code doesn't have it, null if there is no lock.
491 */
492 public function has_lock($key, $identifier) {
493 return (array_key_exists($key, $this->locks) && $this->locks[$key] == $identifier);
494 }
495
496 /**
497 * Returns the path to the lock file.
498 *
499 * @param string $key
500 * @return string The absolute path to use for a lock file for this key.
501 */
502 protected function get_lock_file($key) {
503 return $this->path.'/lock-'.$key.'.lock';
504 }
505
506 /**
507 * Cleans up any left over lock files.
508 *
509 * There shouldn't be any left over lock files but clean them up just in case.
510 */
511 public function __destruct() {
512 $errors = false;
513 foreach ($this->locks as $file) {
514 try {
515 @unlink($file);
516 } catch (Exception $e) {
517 // We just want to ensure we unlink everything possible.
518 $errors = true;
519 }
520 }
521 if ($errors) {
522 error_log('ERROR ERROR ERROR!!! Unable to release all file cache store locks!');
523 }
524 }
525
526 /**
527 * Purges the cache deleting all items within it.
528 *
529 * @return boolean True on success. False otherwise.
530 */
531 public function purge() {
532 $pattern = $this->path.'/*.cache';
533 foreach (glob($pattern, GLOB_MARK | GLOB_NOSORT) as $filename) {
534 @unlink($filename);
535 }
536 $this->keys = array();
537 return true;
538 }
539
540 /**
541 * Checks to make sure that the path for the file cache exists.
542 *
543 * @return bool
544 * @throws coding_exception
545 */
546 protected function ensure_path_exists() {
547 if (!is_writable($this->path)) {
548 if ($this->custompath && !$this->autocreate) {
549 throw new coding_exception('File store path does not exist. You must create it and make it writable to the web server.');
550 }
551 if (!make_writable_directory($this->path, false)) {
552 throw new coding_exception('File store path does not exist and can not be created.');
553 }
554 }
555 return true;
556 }
557
558 /**
559 * Returns true if the user can add an instance of the store plugin.
560 *
561 * @return bool
562 */
563 public static function can_add_instance() {
564 return true;
565 }
566
567 /**
568 * Performs any necessary clean up when the store instance is being deleted.
569 *
570 * 1. Purges the cache directory.
571 * 2. Deletes the directory we created for this cache instances data.
572 */
573 public function cleanup() {
574 $this->purge();
575 @rmdir($this->path);
576 }
577
578 /**
579 * Generates an instance of the cache store that can be used for testing.
580 *
581 * Returns an instance of the cache store, or false if one cannot be created.
582 *
583 * @param cache_definition $definition
584 * @return cache_store_file
585 */
586 public static function initialise_test_instance(cache_definition $definition) {
587 $name = 'File test';
588 $path = make_cache_directory('cache_store_file_test');
589 $cache = new cache_store_file($name, array('path' => $path));
590 $cache->initialise($definition);
591 return $cache;
592 }
593
594 /**
595 * Writes your madness to a file.
596 *
597 * There are several things going on in this function to try to ensure what we don't end up with partial writes etc.
598 * 1. Files for writing are opened with the mode xb, the file must be created and can not already exist.
599 * 2. We use cache_mutex to ensure we acquire a lock.
600 * 3. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed.
601 *
602 * @param string $file Absolute file path
603 * @param string $content The content to write.
604 * @return bool
605 */
606 protected function write_file($file, $content) {
607 // Generate a temp file that is going to be unique. We'll rename it at the end to the desired file name.
608 // in this way we avoid partial writes.
609 $path = dirname($file);
610 while (true) {
611 $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp';
612 if (!file_exists($tempfile)) {
613 break;
614 }
615 }
616
617 // Lock the temp file before we write.
618 if (!cache_lock::lock($tempfile, false)) {
619 return false;
620 }
621
622 // Open the file with mode=x. This acts to create and open the file for writing only.
623 // If the file already exists this will return false.
624 // We also force binary.
625 $handle = @fopen($tempfile, 'xb+');
626 if ($handle === false) {
627 // File already exists... lock already exists, return false.
628 return false;
629 }
630 // We have the lock. Write our content.
631 fwrite($handle, $content);
632 fflush($handle);
633 // Close the handle, we're done.
634 fclose($handle);
635
636 // Unlock the temp file.
637 cache_lock::unlock($tempfile);
638
639 if (md5_file($tempfile) !== md5($content)) {
640 // The md5 of the content of the file must match the md5 of the content given to be written.
641 @unlink($tempfile);
642 return false;
643 }
644
645 // Finally rename the temp file to the desired file, returning the true|false result.
646 $result = rename($tempfile, $file);
647 if (!$result) {
648 // Failed to rename, don't leave files lying around.
649 @unlink($tempfile);
650 }
651 return $result;
652 }
653}