Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402]
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 cache_is_lockable { 42 43 /** 44 * The name of the store. 45 * @var string 46 */ 47 protected $name; 48 49 /** 50 * The path used to store files for this store and the definition it was initialised with. 51 * @var string 52 */ 53 protected $path = false; 54 55 /** 56 * The path in which definition specific sub directories will be created for caching. 57 * @var string 58 */ 59 protected $filestorepath = false; 60 61 /** 62 * Set to true when a prescan has been performed. 63 * @var bool 64 */ 65 protected $prescan = false; 66 67 /** 68 * Set to true if we should store files within a single directory. 69 * By default we use a nested structure in order to reduce the chance of conflicts and avoid any file system 70 * limitations such as maximum files per directory. 71 * @var bool 72 */ 73 protected $singledirectory = false; 74 75 /** 76 * Set to true when the path should be automatically created if it does not yet exist. 77 * @var bool 78 */ 79 protected $autocreate = false; 80 81 /** 82 * Set to true if new cache revision directory needs to be created. Old directory will be purged asynchronously 83 * via Schedule task. 84 * @var bool 85 */ 86 protected $asyncpurge = false; 87 88 /** 89 * Set to true if a custom path is being used. 90 * @var bool 91 */ 92 protected $custompath = false; 93 94 /** 95 * An array of keys we are sure about presently. 96 * @var array 97 */ 98 protected $keys = array(); 99 100 /** 101 * True when the store is ready to be initialised. 102 * @var bool 103 */ 104 protected $isready = false; 105 106 /** 107 * The cache definition this instance has been initialised with. 108 * @var cache_definition 109 */ 110 protected $definition; 111 112 /** 113 * Bytes read or written by last call to set()/get() or set_many()/get_many(). 114 * 115 * @var int 116 */ 117 protected $lastiobytes = 0; 118 119 /** 120 * A reference to the global $CFG object. 121 * 122 * You may be asking yourself why on earth this is here, but there is a good reason. 123 * By holding onto a reference of the $CFG object we can be absolutely sure that it won't be destroyed before 124 * we are done with it. 125 * This makes it possible to use a cache within a destructor method for the purposes of 126 * delayed writes. Like how the session mechanisms work. 127 * 128 * @var stdClass 129 */ 130 private $cfg = null; 131 132 /** @var int Maximum number of seconds to wait for a lock before giving up. */ 133 protected $lockwait = 60; 134 135 /** 136 * Instance of file_lock_factory configured to create locks in the cache directory. 137 * 138 * @var \core\lock\file_lock_factory $lockfactory 139 */ 140 protected $lockfactory = null; 141 142 /** 143 * List of current locks. 144 * 145 * @var array $locks 146 */ 147 protected $locks = []; 148 149 /** 150 * Constructs the store instance. 151 * 152 * Noting that this function is not an initialisation. It is used to prepare the store for use. 153 * The store will be initialised when required and will be provided with a cache_definition at that time. 154 * 155 * @param string $name 156 * @param array $configuration 157 */ 158 public function __construct($name, array $configuration = array()) { 159 global $CFG; 160 161 if (isset($CFG)) { 162 // Hold onto a reference of the global $CFG object. 163 $this->cfg = $CFG; 164 } 165 166 $this->name = $name; 167 if (array_key_exists('path', $configuration) && $configuration['path'] !== '') { 168 $this->custompath = true; 169 $this->autocreate = !empty($configuration['autocreate']); 170 $path = (string)$configuration['path']; 171 if (!is_dir($path)) { 172 if ($this->autocreate) { 173 if (!make_writable_directory($path, false)) { 174 $path = false; 175 debugging('Error trying to autocreate file store path. '.$path, DEBUG_DEVELOPER); 176 } 177 } else { 178 $path = false; 179 debugging('The given file cache store path does not exist. '.$path, DEBUG_DEVELOPER); 180 } 181 } 182 if ($path !== false && !is_writable($path)) { 183 $path = false; 184 debugging('The file cache store path is not writable for `'.$name.'`', DEBUG_DEVELOPER); 185 } 186 } else { 187 $path = make_cache_directory('cachestore_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name)); 188 } 189 $this->isready = $path !== false; 190 $this->filestorepath = $path; 191 // This will be updated once the store has been initialised for a definition. 192 $this->path = $path; 193 194 // Check if we should prescan the directory. 195 if (array_key_exists('prescan', $configuration)) { 196 $this->prescan = (bool)$configuration['prescan']; 197 } else { 198 // Default is no, we should not prescan. 199 $this->prescan = false; 200 } 201 // Check if we should be storing in a single directory. 202 if (array_key_exists('singledirectory', $configuration)) { 203 $this->singledirectory = (bool)$configuration['singledirectory']; 204 } else { 205 // Default: No, we will use multiple directories. 206 $this->singledirectory = false; 207 } 208 // Check if directory needs to be purged asynchronously. 209 if (array_key_exists('asyncpurge', $configuration)) { 210 $this->asyncpurge = (bool)$configuration['asyncpurge']; 211 } else { 212 $this->asyncpurge = false; 213 } 214 215 // Leverage cachelock_file to provide native locking, to avoid duplicating logic. 216 // This will store locks alongside the cache, so local cache uses local locks. 217 $lockdir = $path . '/filelocks'; 218 if (!file_exists($lockdir)) { 219 make_writable_directory($lockdir); 220 } 221 if (array_key_exists('lockwait', $configuration)) { 222 $this->lockwait = (int)$configuration['lockwait']; 223 } 224 $this->lockfactory = new \core\lock\file_lock_factory('cachestore_file', $lockdir); 225 if (!$this->lockfactory->is_available()) { 226 // File locking is disabled in config, fall back to default lock factory. 227 $this->lockfactory = \core\lock\lock_config::get_lock_factory('cachestore_file'); 228 } 229 } 230 231 /** 232 * Performs any necessary operation when the file store instance has been created. 233 */ 234 public function instance_created() { 235 if ($this->isready && !$this->prescan) { 236 // It is supposed the store instance to expect an empty folder. 237 $this->purge_all_definitions(); 238 } 239 } 240 241 /** 242 * Returns true if this store instance is ready to be used. 243 * @return bool 244 */ 245 public function is_ready() { 246 return $this->isready; 247 } 248 249 /** 250 * Returns true once this instance has been initialised. 251 * 252 * @return bool 253 */ 254 public function is_initialised() { 255 return true; 256 } 257 258 /** 259 * Returns the supported features as a combined int. 260 * 261 * @param array $configuration 262 * @return int 263 */ 264 public static function get_supported_features(array $configuration = array()) { 265 $supported = self::SUPPORTS_DATA_GUARANTEE + 266 self::SUPPORTS_NATIVE_TTL + 267 self::IS_SEARCHABLE + 268 self::DEREFERENCES_OBJECTS; 269 return $supported; 270 } 271 272 /** 273 * Returns false as this store does not support multiple identifiers. 274 * (This optional function is a performance optimisation; it must be 275 * consistent with the value from get_supported_features.) 276 * 277 * @return bool False 278 */ 279 public function supports_multiple_identifiers() { 280 return false; 281 } 282 283 /** 284 * Returns the supported modes as a combined int. 285 * 286 * @param array $configuration 287 * @return int 288 */ 289 public static function get_supported_modes(array $configuration = array()) { 290 return self::MODE_APPLICATION + self::MODE_SESSION; 291 } 292 293 /** 294 * Returns true if the store requirements are met. 295 * 296 * @return bool 297 */ 298 public static function are_requirements_met() { 299 return true; 300 } 301 302 /** 303 * Returns true if the given mode is supported by this store. 304 * 305 * @param int $mode One of cache_store::MODE_* 306 * @return bool 307 */ 308 public static function is_supported_mode($mode) { 309 return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION); 310 } 311 312 /** 313 * Initialises the cache. 314 * 315 * Once this has been done the cache is all set to be used. 316 * 317 * @param cache_definition $definition 318 */ 319 public function initialise(cache_definition $definition) { 320 global $CFG; 321 322 $this->definition = $definition; 323 $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id()); 324 $this->path = $this->filestorepath.'/'.$hash; 325 make_writable_directory($this->path, false); 326 327 if ($this->asyncpurge) { 328 $timestampfile = $this->path . '/.lastpurged'; 329 if (!file_exists($timestampfile)) { 330 touch($timestampfile); 331 @chmod($timestampfile, $CFG->filepermissions); 332 } 333 $cacherev = gmdate("YmdHis", filemtime($timestampfile)); 334 // Update file path with new cache revision. 335 $this->path .= '/' . $cacherev; 336 make_writable_directory($this->path, false); 337 } 338 339 if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) { 340 $this->prescan = false; 341 } 342 if ($this->prescan) { 343 $this->prescan_keys(); 344 } 345 } 346 347 /** 348 * Pre-scan the cache to see which keys are present. 349 */ 350 protected function prescan_keys() { 351 $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); 352 if (is_array($files)) { 353 foreach ($files as $filename) { 354 $this->keys[basename($filename)] = filemtime($filename); 355 } 356 } 357 } 358 359 /** 360 * Gets a pattern suitable for use with glob to find all keys in the cache. 361 * 362 * @param string $prefix A prefix to use. 363 * @return string The pattern. 364 */ 365 protected function glob_keys_pattern($prefix = '') { 366 if ($this->singledirectory) { 367 return $this->path . '/'.$prefix.'*.cache'; 368 } else { 369 return $this->path . '/*/'.$prefix.'*.cache'; 370 } 371 } 372 373 /** 374 * Returns the file path to use for the given key. 375 * 376 * @param string $key The key to generate a file path for. 377 * @param bool $create If set to the true the directory structure the key requires will be created. 378 * @return string The full path to the file that stores a particular cache key. 379 */ 380 protected function file_path_for_key($key, $create = false) { 381 if ($this->singledirectory) { 382 // Its a single directory, easy, just the store instances path + the file name. 383 return $this->path . '/' . $key . '.cache'; 384 } else { 385 // We are using a single subdirectory to achieve 1 level. 386 // We suffix the subdir so it does not clash with any windows 387 // reserved filenames like 'con'. 388 $subdir = substr($key, 0, 3) . '-cache'; 389 $dir = $this->path . '/' . $subdir; 390 if ($create) { 391 // Create the directory. This function does it recursivily! 392 make_writable_directory($dir, false); 393 } 394 return $dir . '/' . $key . '.cache'; 395 } 396 } 397 398 /** 399 * Retrieves an item from the cache store given its key. 400 * 401 * @param string $key The key to retrieve 402 * @return mixed The data that was associated with the key, or false if the key did not exist. 403 */ 404 public function get($key) { 405 $this->lastiobytes = 0; 406 $filename = $key.'.cache'; 407 $file = $this->file_path_for_key($key); 408 $ttl = $this->definition->get_ttl(); 409 $maxtime = 0; 410 if ($ttl) { 411 $maxtime = cache::now() - $ttl; 412 } 413 $readfile = false; 414 if ($this->prescan && array_key_exists($filename, $this->keys)) { 415 if ((!$ttl || $this->keys[$filename] >= $maxtime) && file_exists($file)) { 416 $readfile = true; 417 } else { 418 $this->delete($key); 419 } 420 } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) { 421 $readfile = true; 422 } 423 if (!$readfile) { 424 return false; 425 } 426 // Open ensuring the file for reading in binary format. 427 if (!$handle = fopen($file, 'rb')) { 428 return false; 429 } 430 431 // Note: There is no need to perform any file locking here. 432 // The cache file is only ever written to in the `write_file` function, where it does so by writing to a temp 433 // file and performing an atomic rename of that file. The target file is never locked, so there is no benefit to 434 // obtaining a lock (shared or exclusive) here. 435 436 $data = ''; 437 // Read the data in 1Mb chunks. Small caches will not loop more than once. We don't use filesize as it may 438 // be cached with a different value than what we need to read from the file. 439 do { 440 $data .= fread($handle, 1048576); 441 } while (!feof($handle)); 442 $this->lastiobytes = strlen($data); 443 444 if ($this->lastiobytes == 0) { 445 // Potentially statcache is stale. File can be deleted, let's clear cache and recheck. 446 clearstatcache(true, $file); 447 if (!file_exists($file)) { 448 // It's a completely normal condition. Just ignore and keep going. 449 return false; 450 } 451 } 452 453 // Return it unserialised. 454 return $this->prep_data_after_read($data, $file); 455 } 456 457 /** 458 * Retrieves several items from the cache store in a single transaction. 459 * 460 * 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. 461 * 462 * @param array $keys The array of keys to retrieve 463 * @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 464 * be set to false. 465 */ 466 public function get_many($keys) { 467 $result = array(); 468 $total = 0; 469 foreach ($keys as $key) { 470 $result[$key] = $this->get($key); 471 $total += $this->lastiobytes; 472 } 473 $this->lastiobytes = $total; 474 return $result; 475 } 476 477 /** 478 * Gets bytes read by last get() or get_many(), or written by set() or set_many(). 479 * 480 * @return int Bytes read or written 481 * @since Moodle 4.0 482 */ 483 public function get_last_io_bytes(): int { 484 return $this->lastiobytes; 485 } 486 487 /** 488 * Deletes an item from the cache store. 489 * 490 * @param string $key The key to delete. 491 * @return bool Returns true if the operation was a success, false otherwise. 492 */ 493 public function delete($key) { 494 $filename = $key.'.cache'; 495 $file = $this->file_path_for_key($key); 496 if (file_exists($file) && @unlink($file)) { 497 unset($this->keys[$filename]); 498 return true; 499 } 500 501 return false; 502 } 503 504 /** 505 * Deletes several keys from the cache in a single action. 506 * 507 * @param array $keys The keys to delete 508 * @return int The number of items successfully deleted. 509 */ 510 public function delete_many(array $keys) { 511 $count = 0; 512 foreach ($keys as $key) { 513 if ($this->delete($key)) { 514 $count++; 515 } 516 } 517 return $count; 518 } 519 520 /** 521 * Sets an item in the cache given its key and data value. 522 * 523 * @param string $key The key to use. 524 * @param mixed $data The data to set. 525 * @return bool True if the operation was a success false otherwise. 526 */ 527 public function set($key, $data) { 528 $this->ensure_path_exists(); 529 $filename = $key.'.cache'; 530 $file = $this->file_path_for_key($key, true); 531 $serialized = $this->prep_data_before_save($data); 532 $this->lastiobytes = strlen($serialized); 533 $result = $this->write_file($file, $serialized); 534 if (!$result) { 535 // Couldn't write the file. 536 return false; 537 } 538 // Record the key if required. 539 if ($this->prescan) { 540 $this->keys[$filename] = cache::now() + 1; 541 } 542 // Return true.. it all worked **miracles**. 543 return true; 544 } 545 546 /** 547 * Prepares data to be stored in a file. 548 * 549 * @param mixed $data 550 * @return string 551 */ 552 protected function prep_data_before_save($data) { 553 return serialize($data); 554 } 555 556 /** 557 * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save. 558 * 559 * @param string $data 560 * @param string $path 561 * @return mixed 562 */ 563 protected function prep_data_after_read($data, $path) { 564 $result = @unserialize($data); 565 if ($result === false && $data != serialize(false)) { 566 debugging('Failed to unserialise data from cache file: ' . $path . '. Data: ' . $data, DEBUG_DEVELOPER); 567 return false; 568 } 569 return $result; 570 } 571 572 /** 573 * Sets many items in the cache in a single transaction. 574 * 575 * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two 576 * keys, 'key' and 'value'. 577 * @return int The number of items successfully set. It is up to the developer to check this matches the number of items 578 * sent ... if they care that is. 579 */ 580 public function set_many(array $keyvaluearray) { 581 $count = 0; 582 $totaliobytes = 0; 583 foreach ($keyvaluearray as $pair) { 584 if ($this->set($pair['key'], $pair['value'])) { 585 $totaliobytes += $this->lastiobytes; 586 $count++; 587 } 588 } 589 $this->lastiobytes = $totaliobytes; 590 return $count; 591 } 592 593 /** 594 * Checks if the store has a record for the given key and returns true if so. 595 * 596 * @param string $key 597 * @return bool 598 */ 599 public function has($key) { 600 $filename = $key.'.cache'; 601 $maxtime = cache::now() - $this->definition->get_ttl(); 602 if ($this->prescan) { 603 return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime; 604 } 605 $file = $this->file_path_for_key($key); 606 return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime)); 607 } 608 609 /** 610 * Returns true if the store contains records for all of the given keys. 611 * 612 * @param array $keys 613 * @return bool 614 */ 615 public function has_all(array $keys) { 616 foreach ($keys as $key) { 617 if (!$this->has($key)) { 618 return false; 619 } 620 } 621 return true; 622 } 623 624 /** 625 * Returns true if the store contains records for any of the given keys. 626 * 627 * @param array $keys 628 * @return bool 629 */ 630 public function has_any(array $keys) { 631 foreach ($keys as $key) { 632 if ($this->has($key)) { 633 return true; 634 } 635 } 636 return false; 637 } 638 639 /** 640 * Purges the cache definition deleting all the items within it. 641 * 642 * @return boolean True on success. False otherwise. 643 */ 644 public function purge() { 645 global $CFG; 646 if ($this->isready) { 647 // If asyncpurge = true, create a new cache revision directory and adhoc task to delete old directory. 648 if ($this->asyncpurge && isset($this->definition)) { 649 $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id()); 650 $filepath = $this->filestorepath . '/' . $hash; 651 $timestampfile = $filepath . '/.lastpurged'; 652 if (file_exists($timestampfile)) { 653 $oldcacherev = gmdate("YmdHis", filemtime($timestampfile)); 654 $oldcacherevpath = $filepath . '/' . $oldcacherev; 655 // Delete old cache revision file. 656 @unlink($timestampfile); 657 658 // Create adhoc task to delete old cache revision folder. 659 $purgeoldcacherev = new \cachestore_file\task\asyncpurge(); 660 $purgeoldcacherev->set_custom_data(['path' => $oldcacherevpath]); 661 \core\task\manager::queue_adhoc_task($purgeoldcacherev); 662 } 663 touch($timestampfile, time()); 664 @chmod($timestampfile, $CFG->filepermissions); 665 $newcacherev = gmdate("YmdHis", filemtime($timestampfile)); 666 $filepath .= '/' . $newcacherev; 667 make_writable_directory($filepath, false); 668 } else { 669 $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); 670 if (is_array($files)) { 671 foreach ($files as $filename) { 672 @unlink($filename); 673 } 674 } 675 $this->keys = []; 676 } 677 } 678 return true; 679 } 680 681 /** 682 * Purges all the cache definitions deleting all items within them. 683 * 684 * @return boolean True on success. False otherwise. 685 */ 686 protected function purge_all_definitions() { 687 // Warning: limit the deletion to what file store is actually able 688 // to create using the internal {@link purge()} providing the 689 // {@link $path} with a wildcard to perform a purge action over all the definitions. 690 $currpath = $this->path; 691 $this->path = $this->filestorepath.'/*'; 692 $result = $this->purge(); 693 $this->path = $currpath; 694 return $result; 695 } 696 697 /** 698 * Given the data from the add instance form this function creates a configuration array. 699 * 700 * @param stdClass $data 701 * @return array 702 */ 703 public static function config_get_configuration_array($data) { 704 $config = array(); 705 706 if (isset($data->path)) { 707 $config['path'] = $data->path; 708 } 709 if (isset($data->autocreate)) { 710 $config['autocreate'] = $data->autocreate; 711 } 712 if (isset($data->singledirectory)) { 713 $config['singledirectory'] = $data->singledirectory; 714 } 715 if (isset($data->prescan)) { 716 $config['prescan'] = $data->prescan; 717 } 718 if (isset($data->asyncpurge)) { 719 $config['asyncpurge'] = $data->asyncpurge; 720 } 721 if (isset($data->lockwait)) { 722 $config['lockwait'] = $data->lockwait; 723 } 724 725 return $config; 726 } 727 728 /** 729 * Allows the cache store to set its data against the edit form before it is shown to the user. 730 * 731 * @param moodleform $editform 732 * @param array $config 733 */ 734 public static function config_set_edit_form_data(moodleform $editform, array $config) { 735 $data = array(); 736 if (!empty($config['path'])) { 737 $data['path'] = $config['path']; 738 } 739 if (isset($config['autocreate'])) { 740 $data['autocreate'] = (bool)$config['autocreate']; 741 } 742 if (isset($config['singledirectory'])) { 743 $data['singledirectory'] = (bool)$config['singledirectory']; 744 } 745 if (isset($config['prescan'])) { 746 $data['prescan'] = (bool)$config['prescan']; 747 } 748 if (isset($config['asyncpurge'])) { 749 $data['asyncpurge'] = (bool)$config['asyncpurge']; 750 } 751 if (isset($config['lockwait'])) { 752 $data['lockwait'] = (int)$config['lockwait']; 753 } 754 $editform->set_data($data); 755 } 756 757 /** 758 * Checks to make sure that the path for the file cache exists. 759 * 760 * @return bool 761 * @throws coding_exception 762 */ 763 protected function ensure_path_exists() { 764 global $CFG; 765 if (!is_writable($this->path)) { 766 if ($this->custompath && !$this->autocreate) { 767 throw new coding_exception('File store path does not exist. It must exist and be writable by the web server.'); 768 } 769 $createdcfg = false; 770 if (!isset($CFG)) { 771 // This can only happen during destruction of objects. 772 // A cache is being used within a destructor, php is ending a request and $CFG has 773 // already being cleaned up. 774 // Rebuild $CFG with directory permissions just to complete this write. 775 $CFG = $this->cfg; 776 $createdcfg = true; 777 } 778 if (!make_writable_directory($this->path, false)) { 779 throw new coding_exception('File store path does not exist and can not be created.'); 780 } 781 if ($createdcfg) { 782 // We re-created it so we'll clean it up. 783 unset($CFG); 784 } 785 } 786 return true; 787 } 788 789 /** 790 * Performs any necessary clean up when the file store instance is being deleted. 791 * 792 * 1. Purges the cache directory. 793 * 2. Deletes the directory we created for the given definition. 794 */ 795 public function instance_deleted() { 796 $this->purge_all_definitions(); 797 @rmdir($this->filestorepath); 798 } 799 800 /** 801 * Generates an instance of the cache store that can be used for testing. 802 * 803 * Returns an instance of the cache store, or false if one cannot be created. 804 * 805 * @param cache_definition $definition 806 * @return cachestore_file 807 */ 808 public static function initialise_test_instance(cache_definition $definition) { 809 $name = 'File test'; 810 $path = make_cache_directory('cachestore_file_test'); 811 $cache = new cachestore_file($name, array('path' => $path)); 812 if ($cache->is_ready()) { 813 $cache->initialise($definition); 814 } 815 return $cache; 816 } 817 818 /** 819 * Generates the appropriate configuration required for unit testing. 820 * 821 * @return array Array of unit test configuration data to be used by initialise(). 822 */ 823 public static function unit_test_configuration() { 824 return array(); 825 } 826 827 /** 828 * Writes your madness to a file. 829 * 830 * There are several things going on in this function to try to ensure what we don't end up with partial writes etc. 831 * 1. Files for writing are opened with the mode xb, the file must be created and can not already exist. 832 * 2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed. 833 * 834 * @param string $file Absolute file path 835 * @param string $content The content to write. 836 * @return bool 837 */ 838 protected function write_file($file, $content) { 839 // Generate a temp file that is going to be unique. We'll rename it at the end to the desired file name. 840 // in this way we avoid partial writes. 841 $path = dirname($file); 842 while (true) { 843 $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp'; 844 if (!file_exists($tempfile)) { 845 break; 846 } 847 } 848 849 // Open the file with mode=x. This acts to create and open the file for writing only. 850 // If the file already exists this will return false. 851 // We also force binary. 852 $handle = @fopen($tempfile, 'xb+'); 853 if ($handle === false) { 854 // File already exists... lock already exists, return false. 855 return false; 856 } 857 fwrite($handle, $content); 858 fflush($handle); 859 // Close the handle, we're done. 860 fclose($handle); 861 862 if (md5_file($tempfile) !== md5($content)) { 863 // The md5 of the content of the file must match the md5 of the content given to be written. 864 @unlink($tempfile); 865 return false; 866 } 867 868 // Finally rename the temp file to the desired file, returning the true|false result. 869 $result = rename($tempfile, $file); 870 @chmod($file, $this->cfg->filepermissions); 871 if (!$result) { 872 // Failed to rename, don't leave files lying around. 873 @unlink($tempfile); 874 } 875 return $result; 876 } 877 878 /** 879 * Returns the name of this instance. 880 * @return string 881 */ 882 public function my_name() { 883 return $this->name; 884 } 885 886 /** 887 * Finds all of the keys being used by this cache store instance. 888 * 889 * @return array 890 */ 891 public function find_all() { 892 $this->ensure_path_exists(); 893 $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); 894 $return = array(); 895 if ($files === false) { 896 return $return; 897 } 898 foreach ($files as $file) { 899 $return[] = substr(basename($file), 0, -6); 900 } 901 return $return; 902 } 903 904 /** 905 * Finds all of the keys whose keys start with the given prefix. 906 * 907 * @param string $prefix 908 */ 909 public function find_by_prefix($prefix) { 910 $this->ensure_path_exists(); 911 $prefix = preg_replace('#(\*|\?|\[)#', '[$1]', $prefix); 912 $files = glob($this->glob_keys_pattern($prefix), GLOB_MARK | GLOB_NOSORT); 913 $return = array(); 914 if ($files === false) { 915 return $return; 916 } 917 foreach ($files as $file) { 918 // Trim off ".cache" from the end. 919 $return[] = substr(basename($file), 0, -6); 920 } 921 return $return; 922 } 923 924 /** 925 * Gets total size for the directory used by the cache store. 926 * 927 * @return int Total size in bytes 928 */ 929 public function store_total_size(): ?int { 930 return get_directory_size($this->filestorepath); 931 } 932 933 /** 934 * Gets total size for a specific cache. 935 * 936 * With the file cache we can just look at the directory listing without having to 937 * actually load any files, so the $samplekeys parameter is ignored. 938 * 939 * @param int $samplekeys Unused 940 * @return stdClass Cache details 941 */ 942 public function cache_size_details(int $samplekeys = 50): stdClass { 943 $result = (object)[ 944 'supported' => true, 945 'items' => 0, 946 'mean' => 0, 947 'sd' => 0, 948 'margin' => 0 949 ]; 950 951 // Find all the files in this cache. 952 $this->ensure_path_exists(); 953 $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT); 954 if ($files === false || count($files) === 0) { 955 return $result; 956 } 957 958 // Get the sizes and count of files. 959 $sizes = []; 960 foreach ($files as $file) { 961 $result->items++; 962 $sizes[] = filesize($file); 963 } 964 965 // Work out mean and standard deviation. 966 $total = array_sum($sizes); 967 $result->mean = $total / $result->items; 968 $squarediff = 0; 969 foreach ($sizes as $size) { 970 $squarediff += ($size - $result->mean) ** 2; 971 } 972 $squarediff /= $result->items; 973 $result->sd = sqrt($squarediff); 974 return $result; 975 } 976 977 /** 978 * Use lock factory to determine the lock state. 979 * 980 * @param string $key Lock identifier 981 * @param string $ownerid Cache identifier 982 * @return bool|null 983 */ 984 public function check_lock_state($key, $ownerid) : ?bool { 985 if (!array_key_exists($key, $this->locks)) { 986 return null; // Lock does not exist. 987 } 988 if (!array_key_exists($ownerid, $this->locks[$key])) { 989 return false; // Lock exists, but belongs to someone else. 990 } 991 if ($this->locks[$key][$ownerid] instanceof \core\lock\lock) { 992 return true; // Lock exists, and we own it. 993 } 994 // Try to get the lock with an immediate timeout. If this succeeds, the lock does not currently exist. 995 $lock = $this->lockfactory->get_lock($key, 0); 996 if ($lock) { 997 // Lock was not already held. 998 $lock->release(); 999 return null; 1000 } else { 1001 // Lock is held by someone else. 1002 return false; 1003 } 1004 } 1005 1006 /** 1007 * Use lock factory to acquire a lock. 1008 * 1009 * @param string $key Lock identifier 1010 * @param string $ownerid Cache identifier 1011 * @return bool 1012 * @throws cache_exception 1013 */ 1014 public function acquire_lock($key, $ownerid) : bool { 1015 $lock = $this->lockfactory->get_lock($key, $this->lockwait); 1016 if ($lock) { 1017 $this->locks[$key][$ownerid] = $lock; 1018 } 1019 return (bool)$lock; 1020 } 1021 1022 /** 1023 * Use lock factory to release a lock. 1024 * 1025 * @param string $key Lock identifier 1026 * @param string $ownerid Cache identifier 1027 * @return bool 1028 */ 1029 public function release_lock($key, $ownerid) : bool { 1030 if (!array_key_exists($key, $this->locks)) { 1031 return false; // No lock to release. 1032 } 1033 if (!array_key_exists($ownerid, $this->locks[$key])) { 1034 return false; // Tried to release someone else's lock. 1035 } 1036 $unlocked = $this->locks[$key][$ownerid]->release(); 1037 if ($unlocked) { 1038 unset($this->locks[$key]); 1039 } 1040 return $unlocked; 1041 } 1042 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body