Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

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

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