aboutsummaryrefslogtreecommitdiffstats
path: root/lib/private/Cache/CappedMemoryCache.php
blob: 3d843abf5b8ed4596bdd7ea448813ad7d01b4a91 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
<?php
/**
 * @copyright Copyright (c) 2016, ownCloud, Inc.
 *
 * @author Robin Appelman <robin@icewind.nl>
 *
 * @license AGPL-3.0
 *
 * This code is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License, version 3,
 * along with this program. If not, see <http://www.gnu.org/licenses/>
 *
 */

namespace OC\Cache;

use OCP\ICache;

/**
 * In-memory cache with a capacity limit to keep memory usage in check
 *
 * Uses a simple FIFO expiry mechanism
 */
class CappedMemoryCache implements ICache, \ArrayAccess {

	private $capacity;
	private $cache = [];

	public function __construct($capacity = 512) {
		$this->capacity = $capacity;
	}

	public function hasKey($key) {
		return isset($this->cache[$key]);
	}

	public function get($key) {
		return isset($this->cache[$key]) ? $this->cache[$key] : null;
	}

	public function set($key, $value, $ttl = 0) {
		if (is_null($key)) {
			$this->cache[] = $value;
		} else {
			$this->cache[$key] = $value;
		}
		$this->garbageCollect();
	}

	public function remove($key) {
		unset($this->cache[$key]);
		return true;
	}

	public function clear($prefix = '') {
		$this->cache = [];
		return true;
	}

	public function offsetExists($offset) {
		return $this->hasKey($offset);
	}

	public function &offsetGet($offset) {
		return $this->cache[$offset];
	}

	public function offsetSet($offset, $value) {
		$this->set($offset, $value);
	}

	public function offsetUnset($offset) {
		$this->remove($offset);
	}

	public function getData() {
		return $this->cache;
	}


	private function garbageCollect() {
		while (count($this->cache) > $this->capacity) {
			reset($this->cache);
			$key = key($this->cache);
			$this->remove($key);
		}
	}
}
array Associative array ($key => $value) */ protected $cache = []; /** @var array */ protected $envCache = []; /** @var string */ protected $configDir; /** @var string */ protected $configFilePath; /** @var string */ protected $configFileName; /** @var bool */ protected $isReadOnly; /** * @param string $configDir Path to the config dir, needs to end with '/' * @param string $fileName (Optional) Name of the config file. Defaults to config.php */ public function __construct($configDir, $fileName = 'config.php') { $this->configDir = $configDir; $this->configFilePath = $this->configDir.$fileName; $this->configFileName = $fileName; $this->readData(); $this->isReadOnly = $this->getValue('config_is_read_only', false); } /** * Lists all available config keys * * Please note that it does not return the values. * * @return array an array of key names */ public function getKeys() { return array_keys($this->cache); } /** * Returns a config value * * gets its value from an `NC_` prefixed environment variable * if it doesn't exist from config.php * if this doesn't exist either, it will return the given `$default` * * @param string $key key * @param mixed $default = null default value * @return mixed the value or $default */ public function getValue($key, $default = null) { $envKey = self::ENV_PREFIX . $key; if (isset($this->envCache[$envKey])) { return $this->envCache[$envKey]; } if (isset($this->cache[$key])) { return $this->cache[$key]; } return $default; } /** * Sets and deletes values and writes the config.php * * @param array $configs Associative array with `key => value` pairs * If value is null, the config key will be deleted * @throws HintException */ public function setValues(array $configs) { $needsUpdate = false; foreach ($configs as $key => $value) { if ($value !== null) { $needsUpdate |= $this->set($key, $value); } else { $needsUpdate |= $this->delete($key); } } if ($needsUpdate) { // Write changes $this->writeData(); } } /** * Sets the value and writes it to config.php if required * * @param string $key key * @param mixed $value value * @throws HintException */ public function setValue($key, $value) { if ($this->set($key, $value)) { // Write changes $this->writeData(); } } /** * This function sets the value * * @param string $key key * @param mixed $value value * @return bool True if the file needs to be updated, false otherwise * @throws HintException */ protected function set($key, $value) { $this->checkReadOnly(); if (!isset($this->cache[$key]) || $this->cache[$key] !== $value) { // Add change $this->cache[$key] = $value; return true; } return false; } /** * Removes a key from the config and removes it from config.php if required * * @param string $key * @throws HintException */ public function deleteKey($key) { if ($this->delete($key)) { // Write changes $this->writeData(); } } /** * This function removes a key from the config * * @param string $key * @return bool True if the file needs to be updated, false otherwise * @throws HintException */ protected function delete($key) { $this->checkReadOnly(); if (isset($this->cache[$key])) { // Delete key from cache unset($this->cache[$key]); return true; } return false; } /** * Loads the config file * * Reads the config file and saves it to the cache * * @throws \Exception If no lock could be acquired or the config file has not been found */ private function readData() { // Default config should always get loaded $configFiles = [$this->configFilePath]; // Add all files in the config dir ending with the same file name $extra = glob($this->configDir.'*.'.$this->configFileName); if (is_array($extra)) { natsort($extra); $configFiles = array_merge($configFiles, $extra); } // Include file and merge config foreach ($configFiles as $file) { $fileExistsAndIsReadable = file_exists($file) && is_readable($file); $filePointer = $fileExistsAndIsReadable ? fopen($file, 'r') : false; if ($file === $this->configFilePath && $filePointer === false) { // Opening the main config might not be possible, e.g. if the wrong // permissions are set (likely on a new installation) continue; } // Try to acquire a file lock if (!flock($filePointer, LOCK_SH)) { throw new \Exception(sprintf('Could not acquire a shared lock on the config file %s', $file)); } unset($CONFIG); include $file; if (isset($CONFIG) && is_array($CONFIG)) { $this->cache = array_merge($this->cache, $CONFIG); } // Close the file pointer and release the lock flock($filePointer, LOCK_UN); fclose($filePointer); } $this->envCache = getenv(); } /** * Writes the config file * * Saves the config to the config file. * * @throws HintException If the config file cannot be written to * @throws \Exception If no file lock can be acquired */ private function writeData() { $this->checkReadOnly(); // Create a php file ... $content = "<?php\n"; $content .= '$CONFIG = '; $content .= var_export($this->cache, true); $content .= ";\n"; touch($this->configFilePath); $filePointer = fopen($this->configFilePath, 'r+'); // Prevent others not to read the config chmod($this->configFilePath, 0640); // File does not exist, this can happen when doing a fresh install if (!is_resource($filePointer)) { throw new HintException( "Can't write into config directory!", 'This can usually be fixed by giving the webserver write access to the config directory.'); } // Try to acquire a file lock if (!flock($filePointer, LOCK_EX)) { throw new \Exception(sprintf('Could not acquire an exclusive lock on the config file %s', $this->configFilePath)); } // Write the config and release the lock ftruncate($filePointer, 0); fwrite($filePointer, $content); fflush($filePointer); flock($filePointer, LOCK_UN); fclose($filePointer); if (function_exists('opcache_invalidate')) { @opcache_invalidate($this->configFilePath, true); } } /** * @throws HintException */ private function checkReadOnly(): void { if ($this->isReadOnly) { throw new HintException( 'Config is set to be read-only via option "config_is_read_only".', 'Unset "config_is_read_only" to allow changes to the config file.'); } } }