Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   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  }