[ * 'adapter' => 'File', * 'strategies => ['Serializer'] * ] * ]); * ``` * * The path that the cached files will be written to defaults to * `/resources/tmp/cache`, but is user-configurable. * * Note that the cache expiration time is stored within the first few bytes * of the cached data, and is transparently added and/or removed when values * are stored and/or retrieved from the cache. * * @see lithium\storage\cache\adapter */ class File extends \lithium\storage\cache\Adapter { /** * The maximum line length of the file header storing meta data. * * @var integer */ const MAX_HEADER_LENGTH = 500; /** * Constructor. * * @see lithium\storage\Cache::config() * @param array $config Configuration for this cache adapter. These settings are queryable * through `Cache::config('name')`. The available options are as follows: * - `'scope'` _string_: Scope which will prefix keys; per default not set. * - `'expiry'` _mixed_: The default expiration time for cache values, if no value * is otherwise set. Can be either a `strtotime()` compatible tring or TTL in * seconds. To indicate items should not expire use `Cache::PERSIST`. Defaults * to `+1 hour`. * - `'path'` _string_: Path where cached entries live, defaults to * `Libraries::get(true, 'resources') . '/tmp/cache'`. * - `'streams'`: When enabled (by default disabled) read operations will return * stream handles instead of the value itself. This is useful when reading * BLOBs. * @return void */ public function __construct(array $config = []) { $defaults = [ 'path' => Libraries::get(true, 'resources') . '/tmp/cache', 'scope' => null, 'expiry' => '+1 hour', 'streams' => false ]; parent::__construct($config + $defaults); } /** * Generates safe cache keys. * * Keys should be safe to be used as filename. So we conservatively disallalow * any non alphanumeric characters with the exception of dash und underscore. * * We also limit to max. 255 characters. The limit is actually lowered * to 255 minus the length of an crc32b hash minus separator (246) * minus scope length minus separator (246 - x). * * 255 was chosen as most commonly used filesystems (ext2-4, HFS+, * NTFS, XFS, FAT32, btrfs) limit filename characters to a length of * 255. * * @link https://en.wikipedia.org/wiki/Comparison_of_file_systems#Limits * @param array $keys The original keys. * @return array Keys modified and safe to use with adapter. */ public function key(array $keys) { $length = 246 - ($this->_config['scope'] ? strlen($this->_config['scope']) + 1 : 0); return array_map( function($key) use ($length) { $result = substr(preg_replace('/[^a-z0-9_\-]/iu', '_', $key), 0, $length); return $result !== $key ? $result . '_' . hash('crc32b', $key) : $result; }, $keys ); } /** * Write values to the cache. All items to be cached will receive an * expiration time of `$expiry`. * * @param array $keys Key/value pairs with keys to uniquely identify the to-be-cached item. * @param string|integer $expiry A `strtotime()` compatible cache time or TTL in seconds. * To persist an item use `\lithium\storage\Cache::PERSIST`. * @return boolean `true` on successful write, `false` otherwise. */ public function write(array $keys, $expiry = null) { $expiry = $expiry || $expiry === Cache::PERSIST ? $expiry : $this->_config['expiry']; if (!$expiry || $expiry === Cache::PERSIST) { $expires = 0; } elseif (is_int($expiry)) { $expires = $expiry + time(); } else { $expires = strtotime($expiry); } if ($this->_config['scope']) { $keys = $this->_addScopePrefix($this->_config['scope'], $keys, '_'); } foreach ($keys as $key => $value) { if (!$this->_write($key, $value, $expires)) { return false; } } return true; } /** * Read values from the cache. Will attempt to return an array of data * containing key/value pairs of the requested data. * * Invalidates and cleans up expired items on-the-fly when found. * * @param array $keys Keys to uniquely identify the cached items. * @return array Cached values keyed by cache keys on successful read, * keys which could not be read will not be included in * the results array. */ public function read(array $keys) { if ($this->_config['scope']) { $keys = $this->_addScopePrefix($this->_config['scope'], $keys, '_'); } $results = []; foreach ($keys as $key) { if (!$item = $this->_read($key, $this->_config['streams'])) { continue; } if ($item['expiry'] < time() && $item['expiry'] != 0) { $this->_delete($key); continue; } $results[$key] = $item['value']; } if ($this->_config['scope']) { $results = $this->_removeScopePrefix($this->_config['scope'], $results, '_'); } return $results; } /** * Will attempt to remove specified keys from the user space cache. * * @param array $keys Keys to uniquely identify the cached items. * @return boolean `true` on successful delete, `false` otherwise. */ public function delete(array $keys) { if ($this->_config['scope']) { $keys = $this->_addScopePrefix($this->_config['scope'], $keys, '_'); } foreach ($keys as $key) { if (!$this->_delete($key)) { return false; } } return true; } /** * Performs a decrement operation on a specified numeric cache item. * * @param string $key Key of numeric cache item to decrement. * @param integer $offset Offset to decrement - defaults to `1`. * @return integer|boolean The item's new value on successful decrement, else `false`. */ public function decrement($key, $offset = 1) { if ($this->_config['scope']) { $key = "{$this->_config['scope']}_{$key}"; } if (!$result = $this->_read($key)) { return false; } if (!$this->_write($key, $result['value'] -= $offset, $result['expiry'])) { return false; } return $result['value']; } /** * Performs an increment operation on a specified numeric cache item. * * @param string $key Key of numeric cache item to increment * @param integer $offset Offset to increment - defaults to `1`. * @return integer|boolean The item's new value on successful increment, else `false`. */ public function increment($key, $offset = 1) { if ($this->_config['scope']) { $key = "{$this->_config['scope']}_{$key}"; } if (!$result = $this->_read($key)) { return false; } if (!$this->_write($key, $result['value'] += $offset, $result['expiry'])) { return false; } return $result['value']; } /** * Clears entire cache by flushing it. Please note * that a scope - in case one is set - is *not* honored. * * The operation will continue to remove keys even if removing * one single key fails, clearing thoroughly as possible. * * @return boolean `true` on successful clearing, `false` if failed partially or entirely. */ public function clear() { $result = true; foreach (new DirectoryIterator($this->_config['path']) as $file) { if (!$file->isFile()) { continue; } $result = $this->_delete($file->getBasename()) && $result; } return $result; } /** * Cleans entire cache running garbage collection on it. Please * note that a scope - in case one is set - is *not* honored. * * The operation will continue to remove keys even if removing * one single key fails, cleaning thoroughly as possible. * * @return boolean `true` on successful cleaning, `false` if failed partially or entirely. */ public function clean() { $result = true; foreach (new DirectoryIterator($this->_config['path']) as $file) { if (!$file->isFile()) { continue; } if (!$item = $this->_read($key = $file->getBasename())) { continue; } if ($item['expiry'] > time()) { continue; } $result = $this->_delete($key) && $result; } return $result; } /** * Compiles value to format and writes file. * * @see lithium\storage\cache\adapter\File::write() * @param string $key Key to uniquely identify the cached item. * @param mixed $value Value or resource with value to store under given key. * @param integer $expires UNIX timestamp after which the item is invalid. * @return boolean `true` on success, `false` otherwise. */ protected function _write($key, $value, $expires) { $path = "{$this->_config['path']}/{$key}"; if (!$stream = fopen($path, 'wb')) { return false; } fwrite($stream, "{:expiry:{$expires}}\n"); if (is_resource($value)) { stream_copy_to_stream($value, $stream); } else { fwrite($stream, $value); } return fclose($stream); } /** * Reads from file, parses its format and returns its expiry and value. * * @see lithium\storage\cache\adapter\File::read() * @param string $key Key to uniquely identify the cached item. * @param boolean $streams When `true` will return stream handle instead of value. * @return array|boolean Array with `expiry` and `value` or `false` otherwise. */ protected function _read($key, $streams = false) { $path = "{$this->_config['path']}/{$key}"; if (!is_file($path) || !is_readable($path)) { return false; } if (!$stream = fopen($path, 'rb')) { return false; } $header = stream_get_line($stream, static::MAX_HEADER_LENGTH, "\n"); if (!preg_match('/^\{\:expiry\:(\d+)\}/', $header, $matches)) { return false; } if ($streams) { $value = fopen('php://temp', 'wb'); stream_copy_to_stream($stream, $value); rewind($value); } else { $value = stream_get_contents($stream); } fclose($stream); return ['expiry' => $matches[1], 'value' => $value]; } /** * Deletes a file using the corresponding cached item key. * * @see lithium\storage\cache\adapter\File::delete() * @param string $key Key to uniquely identify the cached item. * @return boolean `true` on success, `false` otherwise. */ protected function _delete($key) { $path = "{$this->_config['path']}/{$key}"; return is_readable($path) && is_file($path) && unlink($path); } } ?>