PageRenderTime 43ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 1ms

/cache/stores/file/lib.php

https://bitbucket.org/kudutest1/moodlegit
PHP | 690 lines | 330 code | 52 blank | 308 comment | 71 complexity | c3f299a38d12bffaae1d5aed8708880f MD5 | raw file
  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. * The library file for the file cache store.
  18. *
  19. * This file is part of the file cache store, it contains the API for interacting with an instance of the store.
  20. * This is used as a default cache store within the Cache API. It should never be deleted.
  21. *
  22. * @package cachestore_file
  23. * @category cache
  24. * @copyright 2012 Sam Hemelryk
  25. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  26. */
  27. /**
  28. * The file store class.
  29. *
  30. * Configuration options
  31. * path: string: path to the cache directory, if left empty one will be created in the cache directory
  32. * autocreate: true, false
  33. * prescan: true, false
  34. *
  35. * @copyright 2012 Sam Hemelryk
  36. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  37. */
  38. class cachestore_file extends cache_store implements cache_is_key_aware, cache_is_configurable {
  39. /**
  40. * The name of the store.
  41. * @var string
  42. */
  43. protected $name;
  44. /**
  45. * The path used to store files for this store and the definition it was initialised with.
  46. * @var string
  47. */
  48. protected $path = false;
  49. /**
  50. * The path in which definition specific sub directories will be created for caching.
  51. * @var string
  52. */
  53. protected $filestorepath = false;
  54. /**
  55. * Set to true when a prescan has been performed.
  56. * @var bool
  57. */
  58. protected $prescan = false;
  59. /**
  60. * Set to true if we should store files within a single directory.
  61. * By default we use a nested structure in order to reduce the chance of conflicts and avoid any file system
  62. * limitations such as maximum files per directory.
  63. * @var bool
  64. */
  65. protected $singledirectory = false;
  66. /**
  67. * Set to true when the path should be automatically created if it does not yet exist.
  68. * @var bool
  69. */
  70. protected $autocreate = false;
  71. /**
  72. * Set to true if a custom path is being used.
  73. * @var bool
  74. */
  75. protected $custompath = false;
  76. /**
  77. * An array of keys we are sure about presently.
  78. * @var array
  79. */
  80. protected $keys = array();
  81. /**
  82. * True when the store is ready to be initialised.
  83. * @var bool
  84. */
  85. protected $isready = false;
  86. /**
  87. * The cache definition this instance has been initialised with.
  88. * @var cache_definition
  89. */
  90. protected $definition;
  91. /**
  92. * Constructs the store instance.
  93. *
  94. * Noting that this function is not an initialisation. It is used to prepare the store for use.
  95. * The store will be initialised when required and will be provided with a cache_definition at that time.
  96. *
  97. * @param string $name
  98. * @param array $configuration
  99. */
  100. public function __construct($name, array $configuration = array()) {
  101. $this->name = $name;
  102. if (array_key_exists('path', $configuration) && $configuration['path'] !== '') {
  103. $this->custompath = true;
  104. $this->autocreate = !empty($configuration['autocreate']);
  105. $path = (string)$configuration['path'];
  106. if (!is_dir($path)) {
  107. if ($this->autocreate) {
  108. if (!make_writable_directory($path, false)) {
  109. $path = false;
  110. debugging('Error trying to autocreate file store path. '.$path, DEBUG_DEVELOPER);
  111. }
  112. } else {
  113. $path = false;
  114. debugging('The given file cache store path does not exist. '.$path, DEBUG_DEVELOPER);
  115. }
  116. }
  117. if ($path !== false && !is_writable($path)) {
  118. $path = false;
  119. debugging('The file cache store path is not writable for `'.$name.'`', DEBUG_DEVELOPER);
  120. }
  121. } else {
  122. $path = make_cache_directory('cachestore_file/'.preg_replace('#[^a-zA-Z0-9\.\-_]+#', '', $name));
  123. }
  124. $this->isready = $path !== false;
  125. $this->filestorepath = $path;
  126. // This will be updated once the store has been initialised for a definition.
  127. $this->path = $path;
  128. // Check if we should prescan the directory.
  129. if (array_key_exists('prescan', $configuration)) {
  130. $this->prescan = (bool)$configuration['prescan'];
  131. } else {
  132. // Default is no, we should not prescan.
  133. $this->prescan = false;
  134. }
  135. // Check if we should be storing in a single directory.
  136. if (array_key_exists('singledirectory', $configuration)) {
  137. $this->singledirectory = (bool)$configuration['singledirectory'];
  138. } else {
  139. // Default: No, we will use multiple directories.
  140. $this->singledirectory = false;
  141. }
  142. }
  143. /**
  144. * Performs any necessary operation when the file store instance has been created.
  145. */
  146. public function instance_created() {
  147. if ($this->isready && !$this->prescan) {
  148. // It is supposed the store instance to expect an empty folder.
  149. $this->purge_all_definitions();
  150. }
  151. }
  152. /**
  153. * Returns true if this store instance is ready to be used.
  154. * @return bool
  155. */
  156. public function is_ready() {
  157. return $this->isready;
  158. }
  159. /**
  160. * Returns true once this instance has been initialised.
  161. *
  162. * @return bool
  163. */
  164. public function is_initialised() {
  165. return true;
  166. }
  167. /**
  168. * Returns the supported features as a combined int.
  169. *
  170. * @param array $configuration
  171. * @return int
  172. */
  173. public static function get_supported_features(array $configuration = array()) {
  174. $supported = self::SUPPORTS_DATA_GUARANTEE +
  175. self::SUPPORTS_NATIVE_TTL;
  176. return $supported;
  177. }
  178. /**
  179. * Returns the supported modes as a combined int.
  180. *
  181. * @param array $configuration
  182. * @return int
  183. */
  184. public static function get_supported_modes(array $configuration = array()) {
  185. return self::MODE_APPLICATION + self::MODE_SESSION;
  186. }
  187. /**
  188. * Returns true if the store requirements are met.
  189. *
  190. * @return bool
  191. */
  192. public static function are_requirements_met() {
  193. return true;
  194. }
  195. /**
  196. * Returns true if the given mode is supported by this store.
  197. *
  198. * @param int $mode One of cache_store::MODE_*
  199. * @return bool
  200. */
  201. public static function is_supported_mode($mode) {
  202. return ($mode === self::MODE_APPLICATION || $mode === self::MODE_SESSION);
  203. }
  204. /**
  205. * Initialises the cache.
  206. *
  207. * Once this has been done the cache is all set to be used.
  208. *
  209. * @param cache_definition $definition
  210. */
  211. public function initialise(cache_definition $definition) {
  212. $this->definition = $definition;
  213. $hash = preg_replace('#[^a-zA-Z0-9]+#', '_', $this->definition->get_id());
  214. $this->path = $this->filestorepath.'/'.$hash;
  215. make_writable_directory($this->path);
  216. if ($this->prescan && $definition->get_mode() !== self::MODE_REQUEST) {
  217. $this->prescan = false;
  218. }
  219. if ($this->prescan) {
  220. $this->prescan_keys();
  221. }
  222. }
  223. /**
  224. * Pre-scan the cache to see which keys are present.
  225. */
  226. protected function prescan_keys() {
  227. $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
  228. if (is_array($files)) {
  229. foreach ($files as $filename) {
  230. $this->keys[basename($filename)] = filemtime($filename);
  231. }
  232. }
  233. }
  234. /**
  235. * Gets a pattern suitable for use with glob to find all keys in the cache.
  236. * @return string The pattern.
  237. */
  238. protected function glob_keys_pattern() {
  239. if ($this->singledirectory) {
  240. return $this->path . '/*.cache';
  241. } else {
  242. return $this->path . '/*/*.cache';
  243. }
  244. }
  245. /**
  246. * Returns the file path to use for the given key.
  247. *
  248. * @param string $key The key to generate a file path for.
  249. * @param bool $create If set to the true the directory structure the key requires will be created.
  250. * @return string The full path to the file that stores a particular cache key.
  251. */
  252. protected function file_path_for_key($key, $create = false) {
  253. if ($this->singledirectory) {
  254. // Its a single directory, easy, just the store instances path + the file name.
  255. return $this->path . '/' . $key . '.cache';
  256. } else {
  257. // We are using a single subdirectory to achieve 1 level.
  258. $subdir = substr($key, 0, 3);
  259. $dir = $this->path . '/' . $subdir;
  260. if ($create) {
  261. // Create the directory. This function does it recursivily!
  262. make_writable_directory($dir);
  263. }
  264. return $dir . '/' . $key . '.cache';
  265. }
  266. }
  267. /**
  268. * Retrieves an item from the cache store given its key.
  269. *
  270. * @param string $key The key to retrieve
  271. * @return mixed The data that was associated with the key, or false if the key did not exist.
  272. */
  273. public function get($key) {
  274. $filename = $key.'.cache';
  275. $file = $this->file_path_for_key($key);
  276. $ttl = $this->definition->get_ttl();
  277. if ($ttl) {
  278. $maxtime = cache::now() - $ttl;
  279. }
  280. $readfile = false;
  281. if ($this->prescan && array_key_exists($key, $this->keys)) {
  282. if (!$ttl || $this->keys[$filename] >= $maxtime && file_exists($file)) {
  283. $readfile = true;
  284. } else {
  285. $this->delete($key);
  286. }
  287. } else if (file_exists($file) && (!$ttl || filemtime($file) >= $maxtime)) {
  288. $readfile = true;
  289. }
  290. if (!$readfile) {
  291. return false;
  292. }
  293. // Check the filesize first, likely not needed but important none the less.
  294. $filesize = filesize($file);
  295. if (!$filesize) {
  296. return false;
  297. }
  298. // Open ensuring the file for writing, truncating it and setting the pointer to the start.
  299. if (!$handle = fopen($file, 'rb')) {
  300. return false;
  301. }
  302. // Lock it up!
  303. // We don't care if this succeeds or not, on some systems it will, on some it won't, meah either way.
  304. flock($handle, LOCK_SH);
  305. // HACK ALERT
  306. // There is a problem when reading from the file during PHPUNIT tests. For one reason or another the filesize is not correct
  307. // Doesn't happen during normal operation, just during unit tests.
  308. // Read it.
  309. $data = fread($handle, $filesize+128);
  310. // Unlock it.
  311. flock($handle, LOCK_UN);
  312. // Return it unserialised.
  313. return $this->prep_data_after_read($data);
  314. }
  315. /**
  316. * Retrieves several items from the cache store in a single transaction.
  317. *
  318. * 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.
  319. *
  320. * @param array $keys The array of keys to retrieve
  321. * @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
  322. * be set to false.
  323. */
  324. public function get_many($keys) {
  325. $result = array();
  326. foreach ($keys as $key) {
  327. $result[$key] = $this->get($key);
  328. }
  329. return $result;
  330. }
  331. /**
  332. * Deletes an item from the cache store.
  333. *
  334. * @param string $key The key to delete.
  335. * @return bool Returns true if the operation was a success, false otherwise.
  336. */
  337. public function delete($key) {
  338. $filename = $key.'.cache';
  339. $file = $this->file_path_for_key($key);
  340. if (@unlink($file)) {
  341. unset($this->keys[$filename]);
  342. return true;
  343. }
  344. return false;
  345. }
  346. /**
  347. * Deletes several keys from the cache in a single action.
  348. *
  349. * @param array $keys The keys to delete
  350. * @return int The number of items successfully deleted.
  351. */
  352. public function delete_many(array $keys) {
  353. $count = 0;
  354. foreach ($keys as $key) {
  355. if ($this->delete($key)) {
  356. $count++;
  357. }
  358. }
  359. return $count;
  360. }
  361. /**
  362. * Sets an item in the cache given its key and data value.
  363. *
  364. * @param string $key The key to use.
  365. * @param mixed $data The data to set.
  366. * @return bool True if the operation was a success false otherwise.
  367. */
  368. public function set($key, $data) {
  369. $this->ensure_path_exists();
  370. $filename = $key.'.cache';
  371. $file = $this->file_path_for_key($key, true);
  372. $result = $this->write_file($file, $this->prep_data_before_save($data));
  373. if (!$result) {
  374. // Couldn't write the file.
  375. return false;
  376. }
  377. // Record the key if required.
  378. if ($this->prescan) {
  379. $this->keys[$filename] = cache::now() + 1;
  380. }
  381. // Return true.. it all worked **miracles**.
  382. return true;
  383. }
  384. /**
  385. * Prepares data to be stored in a file.
  386. *
  387. * @param mixed $data
  388. * @return string
  389. */
  390. protected function prep_data_before_save($data) {
  391. return serialize($data);
  392. }
  393. /**
  394. * Prepares the data it has been read from the cache. Undoing what was done in prep_data_before_save.
  395. *
  396. * @param string $data
  397. * @return mixed
  398. * @throws coding_exception
  399. */
  400. protected function prep_data_after_read($data) {
  401. $result = @unserialize($data);
  402. if ($result === false) {
  403. throw new coding_exception('Failed to unserialise data from file. Either failed to read, or failed to write.');
  404. }
  405. return $result;
  406. }
  407. /**
  408. * Sets many items in the cache in a single transaction.
  409. *
  410. * @param array $keyvaluearray An array of key value pairs. Each item in the array will be an associative array with two
  411. * keys, 'key' and 'value'.
  412. * @return int The number of items successfully set. It is up to the developer to check this matches the number of items
  413. * sent ... if they care that is.
  414. */
  415. public function set_many(array $keyvaluearray) {
  416. $count = 0;
  417. foreach ($keyvaluearray as $pair) {
  418. if ($this->set($pair['key'], $pair['value'])) {
  419. $count++;
  420. }
  421. }
  422. return $count;
  423. }
  424. /**
  425. * Checks if the store has a record for the given key and returns true if so.
  426. *
  427. * @param string $key
  428. * @return bool
  429. */
  430. public function has($key) {
  431. $filename = $key.'.cache';
  432. $maxtime = cache::now() - $this->definition->get_ttl();
  433. if ($this->prescan) {
  434. return array_key_exists($filename, $this->keys) && $this->keys[$filename] >= $maxtime;
  435. }
  436. $file = $this->file_path_for_key($key);
  437. return (file_exists($file) && ($this->definition->get_ttl() == 0 || filemtime($file) >= $maxtime));
  438. }
  439. /**
  440. * Returns true if the store contains records for all of the given keys.
  441. *
  442. * @param array $keys
  443. * @return bool
  444. */
  445. public function has_all(array $keys) {
  446. foreach ($keys as $key) {
  447. if (!$this->has($key)) {
  448. return false;
  449. }
  450. }
  451. return true;
  452. }
  453. /**
  454. * Returns true if the store contains records for any of the given keys.
  455. *
  456. * @param array $keys
  457. * @return bool
  458. */
  459. public function has_any(array $keys) {
  460. foreach ($keys as $key) {
  461. if ($this->has($key)) {
  462. return true;
  463. }
  464. }
  465. return false;
  466. }
  467. /**
  468. * Purges the cache definition deleting all the items within it.
  469. *
  470. * @return boolean True on success. False otherwise.
  471. */
  472. public function purge() {
  473. if ($this->isready) {
  474. $files = glob($this->glob_keys_pattern(), GLOB_MARK | GLOB_NOSORT);
  475. if (is_array($files)) {
  476. foreach ($files as $filename) {
  477. @unlink($filename);
  478. }
  479. }
  480. $this->keys = array();
  481. }
  482. return true;
  483. }
  484. /**
  485. * Purges all the cache definitions deleting all items within them.
  486. *
  487. * @return boolean True on success. False otherwise.
  488. */
  489. protected function purge_all_definitions() {
  490. // Warning: limit the deletion to what file store is actually able
  491. // to create using the internal {@link purge()} providing the
  492. // {@link $path} with a wildcard to perform a purge action over all the definitions.
  493. $currpath = $this->path;
  494. $this->path = $this->filestorepath.'/*';
  495. $result = $this->purge();
  496. $this->path = $currpath;
  497. return $result;
  498. }
  499. /**
  500. * Given the data from the add instance form this function creates a configuration array.
  501. *
  502. * @param stdClass $data
  503. * @return array
  504. */
  505. public static function config_get_configuration_array($data) {
  506. $config = array();
  507. if (isset($data->path)) {
  508. $config['path'] = $data->path;
  509. }
  510. if (isset($data->autocreate)) {
  511. $config['autocreate'] = $data->autocreate;
  512. }
  513. if (isset($data->singledirectory)) {
  514. $config['singledirectory'] = $data->singledirectory;
  515. }
  516. if (isset($data->prescan)) {
  517. $config['prescan'] = $data->prescan;
  518. }
  519. return $config;
  520. }
  521. /**
  522. * Allows the cache store to set its data against the edit form before it is shown to the user.
  523. *
  524. * @param moodleform $editform
  525. * @param array $config
  526. */
  527. public static function config_set_edit_form_data(moodleform $editform, array $config) {
  528. $data = array();
  529. if (!empty($config['path'])) {
  530. $data['path'] = $config['path'];
  531. }
  532. if (isset($config['autocreate'])) {
  533. $data['autocreate'] = (bool)$config['autocreate'];
  534. }
  535. if (isset($config['singledirectory'])) {
  536. $data['singledirectory'] = (bool)$config['singledirectory'];
  537. }
  538. if (isset($config['prescan'])) {
  539. $data['prescan'] = (bool)$config['prescan'];
  540. }
  541. $editform->set_data($data);
  542. }
  543. /**
  544. * Checks to make sure that the path for the file cache exists.
  545. *
  546. * @return bool
  547. * @throws coding_exception
  548. */
  549. protected function ensure_path_exists() {
  550. if (!is_writable($this->path)) {
  551. if ($this->custompath && !$this->autocreate) {
  552. throw new coding_exception('File store path does not exist. It must exist and be writable by the web server.');
  553. }
  554. if (!make_writable_directory($this->path, false)) {
  555. throw new coding_exception('File store path does not exist and can not be created.');
  556. }
  557. }
  558. return true;
  559. }
  560. /**
  561. * Performs any necessary clean up when the file store instance is being deleted.
  562. *
  563. * 1. Purges the cache directory.
  564. * 2. Deletes the directory we created for the given definition.
  565. */
  566. public function instance_deleted() {
  567. $this->purge_all_definitions();
  568. @rmdir($this->filestorepath);
  569. }
  570. /**
  571. * Generates an instance of the cache store that can be used for testing.
  572. *
  573. * Returns an instance of the cache store, or false if one cannot be created.
  574. *
  575. * @param cache_definition $definition
  576. * @return cachestore_file
  577. */
  578. public static function initialise_test_instance(cache_definition $definition) {
  579. $name = 'File test';
  580. $path = make_cache_directory('cachestore_file_test');
  581. $cache = new cachestore_file($name, array('path' => $path));
  582. $cache->initialise($definition);
  583. return $cache;
  584. }
  585. /**
  586. * Writes your madness to a file.
  587. *
  588. * There are several things going on in this function to try to ensure what we don't end up with partial writes etc.
  589. * 1. Files for writing are opened with the mode xb, the file must be created and can not already exist.
  590. * 2. Renaming, data is written to a temporary file, where it can be verified using md5 and is then renamed.
  591. *
  592. * @param string $file Absolute file path
  593. * @param string $content The content to write.
  594. * @return bool
  595. */
  596. protected function write_file($file, $content) {
  597. // Generate a temp file that is going to be unique. We'll rename it at the end to the desired file name.
  598. // in this way we avoid partial writes.
  599. $path = dirname($file);
  600. while (true) {
  601. $tempfile = $path.'/'.uniqid(sesskey().'.', true) . '.temp';
  602. if (!file_exists($tempfile)) {
  603. break;
  604. }
  605. }
  606. // Open the file with mode=x. This acts to create and open the file for writing only.
  607. // If the file already exists this will return false.
  608. // We also force binary.
  609. $handle = @fopen($tempfile, 'xb+');
  610. if ($handle === false) {
  611. // File already exists... lock already exists, return false.
  612. return false;
  613. }
  614. fwrite($handle, $content);
  615. fflush($handle);
  616. // Close the handle, we're done.
  617. fclose($handle);
  618. if (md5_file($tempfile) !== md5($content)) {
  619. // The md5 of the content of the file must match the md5 of the content given to be written.
  620. @unlink($tempfile);
  621. return false;
  622. }
  623. // Finally rename the temp file to the desired file, returning the true|false result.
  624. $result = rename($tempfile, $file);
  625. if (!$result) {
  626. // Failed to rename, don't leave files lying around.
  627. @unlink($tempfile);
  628. }
  629. return $result;
  630. }
  631. /**
  632. * Returns the name of this instance.
  633. * @return string
  634. */
  635. public function my_name() {
  636. return $this->name;
  637. }
  638. }