Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 cachestore_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 cachestore_file extends cache_store implements cache_is_key_aware, cache_is_configurable, cache_is_searchable { 41 42 /** 43 * The name of the store. 44 * @var string 45 */ 46 protected $name; 47 48 /** 49 * The path used to store files for this store and the definition it was initialised with. 50 * @var string 51 */ 52 protected $path = false; 53 54 /** 55 * The path in which definition specific sub directories will be created for caching. 56 * @var string 57 */ 58 protected $filestorepath = false; 59 60 /** 61 * Set to true when a prescan has been performed. 62 * @var bool 63 */ 64 protected $prescan = false; 65 66 /** 67 * Set to true if we should store files within a single directory. 68 * By default we use a nested structure in order to reduce the chance of conflicts and avoid any file system 69 * limitations such as maximum files per directory. 70 * @var bool 71 */ 72 protected $singledirectory = false; 73 74 /** 75 * Set to true when the path should be automatically created if it does not yet exist. 76 * @var bool 77 */ 78 protected $autocreate = false; 79 80 /** 81 * Set to true if a custom path is being used. 82 * @var bool 83 */ 84 protected $custompath = false; 85 86 /** 87 * An array of keys we are sure about presently. 88 * @var array 89 */ 90 protected $keys = array(); 91 92 /** 93 * True when the store is ready to be initialised. 94 * @var bool 95 */ 96 protected $isready = false; 97 98 /** 99 * The cache definition this instance has been initialised with. 100 * @var cache_definition 101 */ 102 protected $definition; 103 104 /** 105 * A reference to the global $CFG object. 106 * 107 * You may be asking yourself why on earth this is here, but there is a good reason. 108 * By holding onto a reference of the $CFG object we can be absolutely sure that it won't be destroyed before 109 * we are done with it. 110 * This makes it possible to use a cache within a destructor method for the purposes of 111 * delayed writes. Like how the session mechanisms work. 112 * 113 * @var stdClass 114 */ 115 private $cfg = null; 116 117 /** 118 * Constructs the store instance. 119 * 120 * Noting that this function is not an initialisation. It is used to prepare the store for use. 121 * The store will be initialised when required and will be provided with a cache_definition at that time. 122 * 123 * @param string $name 124 * @param array $configuration 125 */ 126 public function __construct($name, array $configuration = array()) { 127 global $CFG; 128 129 if (isset($CFG)) { 130 // Hold onto a reference of the global $CFG object. 131 $this->cfg = $CFG; 132 } 133 134 $this->name = $name; 135 if (array_key_exists('path', $configuration) && $configuration['path'] !== '') { 136 $this->custompath = true; 137 $this->autocreate = !empty($configuration['autocreate']); 138 $path = (string)$configuration['path']; 139 if (!is_dir($path)) { 140 if ($this->autocreate) { 141 if (!make_writable_directory($path, false)) { 142 $path = false; 143 debugging('Error trying to autocreate file store path. '.$path, DEBUG_DEVELOPER); 144 } 145 } else { 146 $path = false; 147 debugging('The given file cache store path does not exist. '.$path, DEBUG_DEVELOPER); 148 } 149 } 150 if ($path !== false && !is_writable($path)) { 151 $path = false; 152 debugging('The file cache store path is not writable for `'.$name.'`', DEBUG_DEVELOPER); 153 } 154 } else { 155 $path = make_cache_directory('cachestore_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name)); 156 } 157 $this->isready = $path !== false; 158 $this->filestorepath = $path; 159 // This will be updated once the store has been initialised for a definition. 160 $this->path = $path; 161 162 // Check if we should prescan the directory. 163 if (array_key_exists('prescan', $configuration)) { 164 $this->prescan = (bool)$configuration['prescan']; 165 } else { 166 // Default is no, we should not prescan. 167 $this->prescan = false; 168 } 169 // Check if we should be storing in a single directory. 170 if (array_key_exists('singledirectory', $configuration)) { 171 $this->singledirectory = (bool)$configuration['singledirectory']; 172 } else { 173 // Default: No, we will use multiple directories. 174 $this->singledirectory = false; 175 } 176 } 177 178 /** 179 * Performs any necessary operation when the file store instance has been created. 180 */ 181 public function instance_created() { 182 if ($this->isready && !$this->prescan) { 183 // It is supposed the store instance to expect an empty folder. 184 $this->purge_all_definitions(); 185 } 186 } 187 188 /** 189 * Returns true if this store instance is ready to be used. 190 * @return bool 191 */ 192 public function is_ready() { 193 return $this->isready; 194 } 195 196 /** 197 * Returns true once this instance has been initialised. 198 * 199 * @return bool 200 */ 201 public function is_initialised() { 202 return true; 203 } 204 205 /** 206 * Returns the supported features as a combined int. 207 * 208 * @param array $configuration 209 * @return int 210 */ 211 public static function get_supported_features(array $configuration = array()) { 212 $supported = self::SUPPORTS_DATA_GUARANTEE + 213 self::SUPPORTS_NATIVE_TTL + 214 self::IS_SEARCHABLE + 215 self::DEREFERENCES_OBJECTS; 216 return $supported; 217 } 218 219 /** 220 * Returns false as this store does not support multiple identifiers. 221 * (This optional function is a performance optimisation; it must be 222 * consistent with the value from get_supported_features.) 223 * 224 * @return bool False 225 */ 226 public function supports_multiple_identifiers() { 227 return false; 228 } 229 230 /** 231 * Returns the supported modes as a combined int. 232 * 233 * @param array $configuration 234 * @return int 235 */ 236 public static function get_supported_modes(array $configuration = array()) { 237 return self::MODE_APPLICATION + self::MODE_SESSION; 238 } 239 240 /** 241 * Returns true if the store requirements are met. 242 * 243 * @return bool 244 */ 245 public static function are_requirements_met() { 246 return true; 247 } 248 249 /** 250 * Returns true if the given mode is supported by this store. 251 * 252 * @param int $mode One of cache_store::MODE_* 253 * @return bool 254 */ 255 public static function is_supported_mode($mode) { 256 return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION); 257 } 258 259 /** 260 * Initialises the cache. 261 * 262 * Once this has been done the cache is all set to be used. 263 * 264 * @param cache_definition $definition 265 */ 266 public function initialise(cache_definition $definition) { 267 $this->definition = $definition; 268 $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id()); 269 $this->path = $this->filestorepath.'/'.$hash; 270 make_writable_directory($this->path, false); 271 if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) { 272 $this->prescan = false; 273 } 274 if ($this->prescan) { 275 $this->prescan_keys(); 276 } 277 } 278 279 /** 280 * Pre-scan the cache to see which keys are present. 281 */ 282 protected function prescan_keys() { 283 $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); 284 if (is_array($files)) { 285 foreach ($files as $filename) { 286 $this->keys[basename($filename)] = filemtime($filename); 287 } 288 } 289 } 290 291 /** 292 * Gets a pattern suitable for use with glob to find all keys in the cache. 293 * 294 * @param string $prefix A prefix to use. 295 * @return string The pattern. 296 */ 297 protected function glob_keys_pattern($prefix = '') { 298 if ($this->singledirectory) { 299 return $this->path . '/'.$prefix.'*.cache'; 300 } else { 301 return $this->path . '/*/'.$prefix.'*.cache'; 302 } 303 } 304 305 /** 306 * Returns the file path to use for the given key. 307 * 308 * @param string $key The key to generate a file path for. 309 * @param bool $create If set to the true the directory structure the key requires will be created. 310 * @return string The full path to the file that stores a particular cache key. 311 */ 312 protected function file_path_for_key($key, $create = false) { 313 if ($this->singledirectory) { 314 // Its a single directory, easy, just the store instances path + the file name. 315 return $this->path . '/' . $key . '.cache'; 316 } else { 317 // We are using a single subdirectory to achieve 1 level. 318 // We suffix the subdir so it does not clash with any windows 319 // reserved filenames like 'con'. 320 $subdir = substr($key, 0, 3) . '-cache'; 321 $dir = $this->path . '/' . $subdir; 322 if ($create) { 323 // Create the directory. This function does it recursivily! 324 make_writable_directory($dir, false); 325 } 326 return $dir . '/' . $key . '.cache'; 327 } 328 } 329 330 /** 331 * Retrieves an item from the cache store given its key. 332 * 333 * @param string $key The key to retrieve 334 * @return mixed The data that was associated with the key, or false if the key did not exist. 335 */ 336 public function get($key) { 337 $filename = $key.'.cache'; 338 $file = $this->file_path_for_key($key); 339 $ttl = $this->definition->get_ttl(); 340 $maxtime = 0; 341 if ($ttl) { 342 $maxtime = cache::now() - $ttl; 343 } 344 $readfile = false; 345 if ($this->prescan && array_key_exists($filename, $this->keys)) { 346 if ((!$ttl || $this->keys[$filename] >= $maxtime) && file_exists($file)) { 347 $readfile = true; 348 } else { 349 $this->delete($key); 350 } 351 } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) { 352 $readfile = true; 353 } 354 if (!$readfile) { 355 return false; 356 } 357 // Open ensuring the file for reading in binary format. 358 if (!$handle = fopen($file, 'rb')) { 359 return false; 360 } 361 362 // Note: There is no need to perform any file locking here. 363 // The cache file is only ever written to in the `write_file` function, where it does so by writing to a temp 364 // file and performing an atomic rename of that file. The target file is never locked, so there is no benefit to 365 // obtaining a lock (shared or exclusive) here. 366 367 $data = ''; 368 // Read the data in 1Mb chunks. Small caches will not loop more than once. We don't use filesize as it may 369 // be cached with a different value than what we need to read from the file. 370 do { 371 $data .= fread($handle, 1048576); 372 } while (!feof($handle)); 373 374 // Return it unserialised. 375 return $this->prep_data_after_read($data); 376 } 377 378 /** 379 * Retrieves several items from the cache store in a single transaction. 380 * 381 * 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. 382 * 383 * @param array $keys The array of keys to retrieve 384 * @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 385 * be set to false. 386 */ 387 public function get_many($keys) { 388 $result = array(); 389 foreach ($keys as $key) { 390 $result[$key] = $this->get($key); 391 } 392 return $result; 393 } 394 395 /** 396 * Deletes an item from the cache store. 397 * 398 * @param string $key The key to delete. 399 * @return bool Returns true if the operation was a success, false otherwise. 400 */ 401 public function delete($key) { 402 $filename = $key.'.cache'; 403 $file = $this->file_path_for_key($key); 404 if (file_exists($file) && @unlink($file)) { 405 unset($this->keys[$filename]); 406 return true; 407 } 408 409 return false; 410 } 411 412 /** 413 * Deletes several keys from the cache in a single action. 414 * 415 * @param array $keys The keys to delete 416 * @return int The number of items successfully deleted. 417 */ 418 public function delete_many(array $keys) { 419 $count = 0; 420 foreach ($keys as $key) { 421 if ($this->delete($key)) { 422 $count++; 423 } 424 } 425 return $count; 426 } 427 428 /** 429 * Sets an item in the cache given its key and data value. 430 * 431 * @param string $key The key to use. 432 * @param mixed $data The data to set. 433 * @return bool True if the operation was a success false otherwise. 434 */ 435 public function set($key, $data) { 436 $this->ensure_path_exists(); 437 $filename = $key.'.cache'; 438 $file = $this->file_path_for_key($key, true); 439 $result = $this->write_file($file, $this->prep_data_before_save($data)); 440 if (!$result) { 441 // Couldn't write the file. 442 return false; 443 } 444 // Record the key if required. 445 if ($this->prescan) { 446 $this->keys[$filename] = cache::now() + 1; 447 } 448 // Return true.. it all worked **miracles**. 449 return true; 450 } 451 452 /** 453 * Prepares data to be stored in a file. 454 * 455 * @param mixed $data 456 * @return string 457 */ 458 protected function prep_data_before_save($data) { 459 return serialize($data); 460 } 461 462 /** 463 * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save. 464 * 465 * @param string $data 466 * @return mixed 467 * @throws coding_exception 468 */ 469 protected function prep_data_after_read($data) { 470 $result = @unserialize($data); 471 if ($result === false && $data != serialize(false)) { 472 throw new coding_exception('Failed to unserialise data from file. Either failed to read, or failed to write.'); 473 } 474 return $result; 475 } 476 477 /** 478 * Sets many items in the cache in a single transaction. 479 * 480 * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two 481 * keys, 'key' and 'value'. 482 * @return int The number of items successfully set. It is up to the developer to check this matches the number of items 483 * sent ... if they care that is. 484 */ 485 public function set_many(array $keyvaluearray) { 486 $count = 0; 487 foreach ($keyvaluearray as $pair) { 488 if ($this->set($pair['key'], $pair['value'])) { 489 $count++; 490 } 491 } 492 return $count; 493 } 494 495 /** 496 * Checks if the store has a record for the given key and returns true if so. 497 * 498 * @param string $key 499 * @return bool 500 */ 501 public function has($key) { 502 $filename = $key.'.cache'; 503 $maxtime = cache::now() - $this->definition->get_ttl(); 504 if ($this->prescan) { 505 return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime; 506 } 507 $file = $this->file_path_for_key($key); 508 return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime)); 509 } 510 511 /** 512 * Returns true if the store contains records for all of the given keys. 513 * 514 * @param array $keys 515 * @return bool 516 */ 517 public function has_all(array $keys) { 518 foreach ($keys as $key) { 519 if (!$this->has($key)) { 520 return false; 521 } 522 } 523 return true; 524 } 525 526 /** 527 * Returns true if the store contains records for any of the given keys. 528 * 529 * @param array $keys 530 * @return bool 531 */ 532 public function has_any(array $keys) { 533 foreach ($keys as $key) { 534 if ($this->has($key)) { 535 return true; 536 } 537 } 538 return false; 539 } 540 541 /** 542 * Purges the cache definition deleting all the items within it. 543 * 544 * @return boolean True on success. False otherwise. 545 */ 546 public function purge() { 547 if ($this->isready) { 548 $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); 549 if (is_array($files)) { 550 foreach ($files as $filename) { 551 @unlink($filename); 552 } 553 } 554 $this->keys = array(); 555 } 556 return true; 557 } 558 559 /** 560 * Purges all the cache definitions deleting all items within them. 561 * 562 * @return boolean True on success. False otherwise. 563 */ 564 protected function purge_all_definitions() { 565 // Warning: limit the deletion to what file store is actually able 566 // to create using the internal {@link purge()} providing the 567 // {@link $path} with a wildcard to perform a purge action over all the definitions. 568 $currpath = $this->path; 569 $this->path = $this->filestorepath.'/*'; 570 $result = $this->purge(); 571 $this->path = $currpath; 572 return $result; 573 } 574 575 /** 576 * Given the data from the add instance form this function creates a configuration array. 577 * 578 * @param stdClass $data 579 * @return array 580 */ 581 public static function config_get_configuration_array($data) { 582 $config = array(); 583 584 if (isset($data->path)) { 585 $config['path'] = $data->path; 586 } 587 if (isset($data->autocreate)) { 588 $config['autocreate'] = $data->autocreate; 589 } 590 if (isset($data->singledirectory)) { 591 $config['singledirectory'] = $data->singledirectory; 592 } 593 if (isset($data->prescan)) { 594 $config['prescan'] = $data->prescan; 595 } 596 597 return $config; 598 } 599 600 /** 601 * Allows the cache store to set its data against the edit form before it is shown to the user. 602 * 603 * @param moodleform $editform 604 * @param array $config 605 */ 606 public static function config_set_edit_form_data(moodleform $editform, array $config) { 607 $data = array(); 608 if (!empty($config['path'])) { 609 $data['path'] = $config['path']; 610 } 611 if (isset($config['autocreate'])) { 612 $data['autocreate'] = (bool)$config['autocreate']; 613 } 614 if (isset($config['singledirectory'])) { 615 $data['singledirectory'] = (bool)$config['singledirectory']; 616 } 617 if (isset($config['prescan'])) { 618 $data['prescan'] = (bool)$config['prescan']; 619 } 620 $editform->set_data($data); 621 } 622 623 /** 624 * Checks to make sure that the path for the file cache exists. 625 * 626 * @return bool 627 * @throws coding_exception 628 */ 629 protected function ensure_path_exists() { 630 global $CFG; 631 if (!is_writable($this->path)) { 632 if ($this->custompath && !$this->autocreate) { 633 throw new coding_exception('File store path does not exist. It must exist and be writable by the web server.'); 634 } 635 $createdcfg = false; 636 if (!isset($CFG)) { 637 // This can only happen during destruction of objects. 638 // A cache is being used within a destructor, php is ending a request and $CFG has 639 // already being cleaned up. 640 // Rebuild $CFG with directory permissions just to complete this write. 641 $CFG = $this->cfg; 642 $createdcfg = true; 643 } 644 if (!make_writable_directory($this->path, false)) { 645 throw new coding_exception('File store path does not exist and can not be created.'); 646 } 647 if ($createdcfg) { 648 // We re-created it so we'll clean it up. 649 unset($CFG); 650 } 651 } 652 return true; 653 } 654 655 /** 656 * Performs any necessary clean up when the file store instance is being deleted. 657 * 658 * 1. Purges the cache directory. 659 * 2. Deletes the directory we created for the given definition. 660 */ 661 public function instance_deleted() { 662 $this->purge_all_definitions(); 663 @rmdir($this->filestorepath); 664 } 665 666 /** 667 * Generates an instance of the cache store that can be used for testing. 668 * 669 * Returns an instance of the cache store, or false if one cannot be created. 670 * 671 * @param cache_definition $definition 672 * @return cachestore_file 673 */ 674 public static function initialise_test_instance(cache_definition $definition) { 675 $name = 'File test'; 676 $path = make_cache_directory('cachestore_file_test'); 677 $cache = new cachestore_file($name, array('path' => $path)); 678 if ($cache->is_ready()) { 679 $cache->initialise($definition); 680 } 681 return $cache; 682 } 683 684 /** 685 * Generates the appropriate configuration required for unit testing. 686 * 687 * @return array Array of unit test configuration data to be used by initialise(). 688 */ 689 public static function unit_test_configuration() { 690 return array(); 691 } 692 693 /** 694 * Writes your madness to a file. 695 * 696 * There are several things going on in this function to try to ensure what we don't end up with partial writes etc. 697 * 1. Files for writing are opened with the mode xb, the file must be created and can not already exist. 698 * 2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed. 699 * 700 * @param string $file Absolute file path 701 * @param string $content The content to write. 702 * @return bool 703 */ 704 protected function write_file($file, $content) { 705 // Generate a temp file that is going to be unique. We'll rename it at the end to the desired file name. 706 // in this way we avoid partial writes. 707 $path = dirname($file); 708 while (true) { 709 $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp'; 710 if (!file_exists($tempfile)) { 711 break; 712 } 713 } 714 715 // Open the file with mode=x. This acts to create and open the file for writing only. 716 // If the file already exists this will return false. 717 // We also force binary. 718 $handle = @fopen($tempfile, 'xb+'); 719 if ($handle === false) { 720 // File already exists... lock already exists, return false. 721 return false; 722 } 723 fwrite($handle, $content); 724 fflush($handle); 725 // Close the handle, we're done. 726 fclose($handle); 727 728 if (md5_file($tempfile) !== md5($content)) { 729 // The md5 of the content of the file must match the md5 of the content given to be written. 730 @unlink($tempfile); 731 return false; 732 } 733 734 // Finally rename the temp file to the desired file, returning the true|false result. 735 $result = rename($tempfile, $file); 736 @chmod($file, $this->cfg->filepermissions); 737 if (!$result) { 738 // Failed to rename, don't leave files lying around. 739 @unlink($tempfile); 740 } 741 return $result; 742 } 743 744 /** 745 * Returns the name of this instance. 746 * @return string 747 */ 748 public function my_name() { 749 return $this->name; 750 } 751 752 /** 753 * Finds all of the keys being used by this cache store instance. 754 * 755 * @return array 756 */ 757 public function find_all() { 758 $this->ensure_path_exists(); 759 $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); 760 $return = array(); 761 if ($files === false) { 762 return $return; 763 } 764 foreach ($files as $file) { 765 $return[] = substr(basename($file), 0, -6); 766 } 767 return $return; 768 } 769 770 /** 771 * Finds all of the keys whose keys start with the given prefix. 772 * 773 * @param string $prefix 774 */ 775 public function find_by_prefix($prefix) { 776 $this->ensure_path_exists(); 777 $prefix = preg_replace('#(\*|\?|\[)#', '[$1]', $prefix); 778 $files = glob($this->glob_keys_pattern($prefix), GLOB_MARK | GLOB_NOSORT); 779 $return = array(); 780 if ($files === false) { 781 return $return; 782 } 783 foreach ($files as $file) { 784 // Trim off ".cache" from the end. 785 $return[] = substr(basename($file), 0, -6); 786 } 787 return $return; 788 } 789 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body