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