Commit | Line | Data |
---|---|---|
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 | */ | |
40 | class 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 | } |