Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1705 lines (1489 sloc) 50.9 KB
<?php
/**
* @see https://github.com/laminas/laminas-cache for the canonical source repository
* @copyright https://github.com/laminas/laminas-cache/blob/master/COPYRIGHT.md
* @license https://github.com/laminas/laminas-cache/blob/master/LICENSE.md New BSD License
*/
namespace Laminas\Cache\Storage\Adapter;
use ArrayObject;
use Exception as BaseException;
use GlobIterator;
use Laminas\Cache\Exception;
use Laminas\Cache\Storage;
use Laminas\Cache\Storage\AvailableSpaceCapableInterface;
use Laminas\Cache\Storage\Capabilities;
use Laminas\Cache\Storage\ClearByNamespaceInterface;
use Laminas\Cache\Storage\ClearByPrefixInterface;
use Laminas\Cache\Storage\ClearExpiredInterface;
use Laminas\Cache\Storage\FlushableInterface;
use Laminas\Cache\Storage\IterableInterface;
use Laminas\Cache\Storage\OptimizableInterface;
use Laminas\Cache\Storage\TaggableInterface;
use Laminas\Cache\Storage\TotalSpaceCapableInterface;
use Laminas\Stdlib\ErrorHandler;
use stdClass;
class Filesystem extends AbstractAdapter implements
AvailableSpaceCapableInterface,
ClearByNamespaceInterface,
ClearByPrefixInterface,
ClearExpiredInterface,
FlushableInterface,
IterableInterface,
OptimizableInterface,
TaggableInterface,
TotalSpaceCapableInterface
{
/**
* Buffered total space in bytes
*
* @var null|int|float
*/
protected $totalSpace;
/**
* An identity for the last filespec
* (cache directory + namespace prefix + key + directory level)
*
* @var string
*/
protected $lastFileSpecId = '';
/**
* The last used filespec
*
* @var string
*/
protected $lastFileSpec = '';
/**
* Set options.
*
* @param array|\Traversable|FilesystemOptions $options
* @return Filesystem
* @see getOptions()
*/
public function setOptions($options)
{
if (! $options instanceof FilesystemOptions) {
$options = new FilesystemOptions($options);
}
return parent::setOptions($options);
}
/**
* Get options.
*
* @return FilesystemOptions
* @see setOptions()
*/
public function getOptions()
{
if (! $this->options) {
$this->setOptions(new FilesystemOptions());
}
return $this->options;
}
/* FlushableInterface */
/**
* Flush the whole storage
*
* @throws Exception\RuntimeException
* @return bool
*/
public function flush()
{
$flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
$dir = $this->getOptions()->getCacheDir();
$clearFolder = null;
$clearFolder = function ($dir) use (& $clearFolder, $flags) {
$it = new GlobIterator($dir . DIRECTORY_SEPARATOR . '*', $flags);
foreach ($it as $pathname) {
if ($it->isDir()) {
$clearFolder($pathname);
rmdir($pathname);
} else {
// remove the file by ignoring errors if the file doesn't exist afterwards
// to fix a possible race condition if onother process removed the faile already.
ErrorHandler::start();
unlink($pathname);
$err = ErrorHandler::stop();
if ($err && file_exists($pathname)) {
ErrorHandler::addError(
$err->getSeverity(),
$err->getMessage(),
$err->getFile(),
$err->getLine()
);
}
}
}
};
ErrorHandler::start();
$clearFolder($dir);
$error = ErrorHandler::stop();
if ($error) {
throw new Exception\RuntimeException("Flushing directory '{$dir}' failed", 0, $error);
}
return true;
}
/* ClearExpiredInterface */
/**
* Remove expired items
*
* @return bool
*
* @triggers clearExpired.exception(ExceptionEvent)
*/
public function clearExpired()
{
$options = $this->getOptions();
$namespace = $options->getNamespace();
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
$flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
$path = $options->getCacheDir()
. str_repeat(DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel())
. DIRECTORY_SEPARATOR . $prefix
. '*.' . $this->escapeSuffixForGlob($this->getOptions()->getSuffix());
$glob = new GlobIterator($path, $flags);
$time = time();
$ttl = $options->getTtl();
ErrorHandler::start();
foreach ($glob as $pathname) {
// get last modification time of the file but ignore if the file is missing
// to fix a possible race condition if onother process removed the faile already.
ErrorHandler::start();
$mtime = filemtime($pathname);
$err = ErrorHandler::stop();
if ($err && file_exists($pathname)) {
ErrorHandler::addError($err->getSeverity(), $err->getMessage(), $err->getFile(), $err->getLine());
} elseif ($time >= $mtime + $ttl) {
// remove the file by ignoring errors if the file doesn't exist afterwards
// to fix a possible race condition if onother process removed the faile already.
ErrorHandler::start();
unlink($pathname);
$err = ErrorHandler::stop();
if ($err && file_exists($pathname)) {
ErrorHandler::addError($err->getSeverity(), $err->getMessage(), $err->getFile(), $err->getLine());
} else {
$tagPathname = $this->formatTagFilename(substr($pathname, 0, -4));
ErrorHandler::start();
unlink($tagPathname);
$err = ErrorHandler::stop();
if ($err && file_exists($pathname)) {
ErrorHandler::addError(
$err->getSeverity(),
$err->getMessage(),
$err->getFile(),
$err->getLine()
);
}
}
}
}
$error = ErrorHandler::stop();
if ($error) {
$result = false;
return $this->triggerException(
__FUNCTION__,
new ArrayObject(),
$result,
new Exception\RuntimeException('Failed to clear expired items', 0, $error)
);
}
return true;
}
/* ClearByNamespaceInterface */
/**
* Remove items by given namespace
*
* @param string $namespace
* @throws Exception\RuntimeException
* @return bool
*/
public function clearByNamespace($namespace)
{
$namespace = (string) $namespace;
if ($namespace === '') {
throw new Exception\InvalidArgumentException('No namespace given');
}
$options = $this->getOptions();
$prefix = $namespace . $options->getNamespaceSeparator();
$flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
$path = $options->getCacheDir()
. str_repeat(DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel())
. DIRECTORY_SEPARATOR . $prefix . '*.*';
$glob = new GlobIterator($path, $flags);
ErrorHandler::start();
foreach ($glob as $pathname) {
// remove the file by ignoring errors if the file doesn't exist afterwards
// to fix a possible race condition if onother process removed the faile already.
ErrorHandler::start();
unlink($pathname);
$err = ErrorHandler::stop();
if ($err && file_exists($pathname)) {
ErrorHandler::addError($err->getSeverity(), $err->getMessage(), $err->getFile(), $err->getLine());
}
}
$err = ErrorHandler::stop();
if ($err) {
$result = false;
return $this->triggerException(
__FUNCTION__,
new ArrayObject(),
$result,
new Exception\RuntimeException("Failed to clear items of namespace '{$namespace}'", 0, $err)
);
}
return true;
}
/* ClearByPrefixInterface */
/**
* Remove items matching given prefix
*
* @param string $prefix
* @throws Exception\RuntimeException
* @return bool
*/
public function clearByPrefix($prefix)
{
$prefix = (string) $prefix;
if ($prefix === '') {
throw new Exception\InvalidArgumentException('No prefix given');
}
$options = $this->getOptions();
$namespace = $options->getNamespace();
$nsPrefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
$flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
$path = $options->getCacheDir()
. str_repeat(DIRECTORY_SEPARATOR . $nsPrefix . '*', $options->getDirLevel())
. DIRECTORY_SEPARATOR . $nsPrefix . $prefix . '*.*';
$glob = new GlobIterator($path, $flags);
ErrorHandler::start();
foreach ($glob as $pathname) {
// remove the file by ignoring errors if the file doesn't exist afterwards
// to fix a possible race condition if onother process removed the faile already.
ErrorHandler::start();
unlink($pathname);
$err = ErrorHandler::stop();
if ($err && file_exists($pathname)) {
ErrorHandler::addError($err->getSeverity(), $err->getMessage(), $err->getFile(), $err->getLine());
}
}
$err = ErrorHandler::stop();
if ($err) {
$result = false;
return $this->triggerException(
__FUNCTION__,
new ArrayObject(),
$result,
new Exception\RuntimeException("Failed to remove files of '{$path}'", 0, $err)
);
}
return true;
}
/* TaggableInterface */
/**
* Set tags to an item by given key.
* An empty array will remove all tags.
*
* @param string $key
* @param string[] $tags
* @return bool
*/
public function setTags($key, array $tags)
{
$this->normalizeKey($key);
if (! $this->internalHasItem($key)) {
return false;
}
$filespec = $this->getFileSpec($key);
if (! $tags) {
$this->unlink($this->formatTagFilename($filespec));
return true;
}
$this->putFileContent(
$this->formatTagFilename($filespec),
implode("\n", $tags)
);
return true;
}
/**
* Get tags of an item by given key
*
* @param string $key
* @return string[]|FALSE
*/
public function getTags($key)
{
$this->normalizeKey($key);
if (! $this->internalHasItem($key)) {
return false;
}
$filespec = $this->formatTagFilename($this->getFileSpec($key));
$tags = [];
if (file_exists($filespec)) {
$tags = explode("\n", $this->getFileContent($filespec));
}
return $tags;
}
/**
* Remove items matching given tags.
*
* If $disjunction only one of the given tags must match
* else all given tags must match.
*
* @param string[] $tags
* @param bool $disjunction
* @return bool
*/
public function clearByTags(array $tags, $disjunction = false)
{
if (! $tags) {
return true;
}
$tagCount = count($tags);
$options = $this->getOptions();
$namespace = $options->getNamespace();
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
$flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
$path = $options->getCacheDir()
. str_repeat(DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel())
. DIRECTORY_SEPARATOR . $prefix
. '*.' . $this->escapeSuffixForGlob($this->getOptions()->getTagSuffix());
$glob = new GlobIterator($path, $flags);
foreach ($glob as $pathname) {
try {
$diff = array_diff($tags, explode("\n", $this->getFileContent($pathname)));
} catch (Exception\RuntimeException $exception) {
// ignore missing files because of possible raise conditions
// e.g. another process already deleted that item
if (! file_exists($pathname)) {
continue;
}
throw $exception;
}
$rem = false;
if ($disjunction && count($diff) < $tagCount) {
$rem = true;
} elseif (! $disjunction && ! $diff) {
$rem = true;
}
if ($rem) {
unlink($pathname);
$datPathname = $this->formatFilename(substr($pathname, 0, -4));
if (file_exists($datPathname)) {
unlink($datPathname);
}
}
}
return true;
}
/* IterableInterface */
/**
* Get the storage iterator
*
* @return FilesystemIterator
*/
public function getIterator()
{
$options = $this->getOptions();
$namespace = $options->getNamespace();
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
$path = $options->getCacheDir()
. str_repeat(DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel())
. DIRECTORY_SEPARATOR . $prefix
. '*.' . $this->escapeSuffixForGlob($this->getOptions()->getSuffix());
return new FilesystemIterator($this, $path, $prefix);
}
/* OptimizableInterface */
/**
* Optimize the storage
*
* @return bool
* @throws Exception\RuntimeException
*/
public function optimize()
{
$options = $this->getOptions();
if ($options->getDirLevel()) {
$namespace = $options->getNamespace();
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
// removes only empty directories
$this->rmDir($options->getCacheDir(), $prefix);
}
return true;
}
/* TotalSpaceCapableInterface */
/**
* Get total space in bytes
*
* @throws Exception\RuntimeException
* @return int|float
*/
public function getTotalSpace()
{
if ($this->totalSpace === null) {
$path = $this->getOptions()->getCacheDir();
ErrorHandler::start();
$total = disk_total_space($path);
$error = ErrorHandler::stop();
if ($total === false) {
throw new Exception\RuntimeException("Can't detect total space of '{$path}'", 0, $error);
}
$this->totalSpace = $total;
// clean total space buffer on change cache_dir
$events = $this->getEventManager();
$handle = null;
$totalSpace = & $this->totalSpace;
$callback = function ($event) use (& $events, & $handle, & $totalSpace) {
$params = $event->getParams();
if (isset($params['cache_dir'])) {
$totalSpace = null;
$events->detach($handle);
}
};
$events->attach('option', $callback);
}
return $this->totalSpace;
}
/* AvailableSpaceCapableInterface */
/**
* Get available space in bytes
*
* @throws Exception\RuntimeException
* @return float
*/
public function getAvailableSpace()
{
$path = $this->getOptions()->getCacheDir();
ErrorHandler::start();
$avail = disk_free_space($path);
$error = ErrorHandler::stop();
if ($avail === false) {
throw new Exception\RuntimeException("Can't detect free space of '{$path}'", 0, $error);
}
return $avail;
}
/* reading */
/**
* Get an item.
*
* @param string $key
* @param bool $success
* @param mixed $casToken
* @return mixed Data on success, null on failure
* @throws Exception\ExceptionInterface
*
* @triggers getItem.pre(PreEvent)
* @triggers getItem.post(PostEvent)
* @triggers getItem.exception(ExceptionEvent)
*/
public function getItem($key, & $success = null, & $casToken = null)
{
$options = $this->getOptions();
if ($options->getReadable() && $options->getClearStatCache()) {
clearstatcache();
}
$argn = func_num_args();
if ($argn > 2) {
return parent::getItem($key, $success, $casToken);
} elseif ($argn > 1) {
return parent::getItem($key, $success);
}
return parent::getItem($key);
}
/**
* Get multiple items.
*
* @param array $keys
* @return array Associative array of keys and values
* @throws Exception\ExceptionInterface
*
* @triggers getItems.pre(PreEvent)
* @triggers getItems.post(PostEvent)
* @triggers getItems.exception(ExceptionEvent)
*/
public function getItems(array $keys)
{
$options = $this->getOptions();
if ($options->getReadable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::getItems($keys);
}
/**
* Internal method to get an item.
*
* @param string $normalizedKey
* @param bool $success
* @param mixed $casToken
* @return null|mixed Data on success, null on failure
* @throws Exception\ExceptionInterface
* @throws BaseException
*/
protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null)
{
if (! $this->internalHasItem($normalizedKey)) {
$success = false;
return;
}
try {
$filespec = $this->formatFilename($this->getFileSpec($normalizedKey));
$data = $this->getFileContent($filespec);
// use filemtime + filesize as CAS token
if (func_num_args() > 2) {
$casToken = filemtime($filespec) . filesize($filespec);
}
$success = true;
return $data;
} catch (BaseException $e) {
$success = false;
throw $e;
}
}
/**
* Internal method to get multiple items.
*
* @param array $normalizedKeys
* @return array Associative array of keys and values
* @throws Exception\ExceptionInterface
*/
protected function internalGetItems(array & $normalizedKeys)
{
$keys = $normalizedKeys; // Don't change argument passed by reference
$result = [];
while ($keys) {
// LOCK_NB if more than one items have to read
$nonBlocking = count($keys) > 1;
$wouldblock = null;
// read items
foreach ($keys as $i => $key) {
if (! $this->internalHasItem($key)) {
unset($keys[$i]);
continue;
}
$filespec = $this->formatFilename($this->getFileSpec($key));
$data = $this->getFileContent($filespec, $nonBlocking, $wouldblock);
if ($nonBlocking && $wouldblock) {
continue;
} else {
unset($keys[$i]);
}
$result[$key] = $data;
}
// TODO: Don't check ttl after first iteration
// $options['ttl'] = 0;
}
return $result;
}
/**
* Test if an item exists.
*
* @param string $key
* @return bool
* @throws Exception\ExceptionInterface
*
* @triggers hasItem.pre(PreEvent)
* @triggers hasItem.post(PostEvent)
* @triggers hasItem.exception(ExceptionEvent)
*/
public function hasItem($key)
{
$options = $this->getOptions();
if ($options->getReadable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::hasItem($key);
}
/**
* Test multiple items.
*
* @param array $keys
* @return array Array of found keys
* @throws Exception\ExceptionInterface
*
* @triggers hasItems.pre(PreEvent)
* @triggers hasItems.post(PostEvent)
* @triggers hasItems.exception(ExceptionEvent)
*/
public function hasItems(array $keys)
{
$options = $this->getOptions();
if ($options->getReadable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::hasItems($keys);
}
/**
* Internal method to test if an item exists.
*
* @param string $normalizedKey
* @return bool
* @throws Exception\ExceptionInterface
*/
protected function internalHasItem(& $normalizedKey)
{
$file = $this->formatFilename($this->getFileSpec($normalizedKey));
if (! file_exists($file)) {
return false;
}
$ttl = $this->getOptions()->getTtl();
if ($ttl) {
ErrorHandler::start();
$mtime = filemtime($file);
$error = ErrorHandler::stop();
if (! $mtime) {
throw new Exception\RuntimeException("Error getting mtime of file '{$file}'", 0, $error);
}
if (time() >= ($mtime + $ttl)) {
return false;
}
}
return true;
}
/**
* Get metadata
*
* @param string $key
* @return array|bool Metadata on success, false on failure
*/
public function getMetadata($key)
{
$options = $this->getOptions();
if ($options->getReadable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::getMetadata($key);
}
/**
* Get metadatas
*
* @param array $keys
* @param array $options
* @return array Associative array of keys and metadata
*/
public function getMetadatas(array $keys, array $options = [])
{
$options = $this->getOptions();
if ($options->getReadable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::getMetadatas($keys);
}
/**
* Get info by key
*
* @param string $normalizedKey
* @return array|bool Metadata on success, false on failure
*/
protected function internalGetMetadata(& $normalizedKey)
{
if (! $this->internalHasItem($normalizedKey)) {
return false;
}
$options = $this->getOptions();
$filespec = $this->getFileSpec($normalizedKey);
$file = $this->formatFilename($filespec);
$metadata = [
'filespec' => $filespec,
'mtime' => filemtime($file)
];
if (! $options->getNoCtime()) {
$metadata['ctime'] = filectime($file);
}
if (! $options->getNoAtime()) {
$metadata['atime'] = fileatime($file);
}
return $metadata;
}
/**
* Internal method to get multiple metadata
*
* @param array $normalizedKeys
* @return array Associative array of keys and metadata
* @throws Exception\ExceptionInterface
*/
protected function internalGetMetadatas(array & $normalizedKeys)
{
$options = $this->getOptions();
$result = [];
foreach ($normalizedKeys as $normalizedKey) {
$filespec = $this->getFileSpec($normalizedKey);
$file = $this->formatFilename($filespec);
$metadata = [
'filespec' => $filespec,
'mtime' => filemtime($file),
];
if (! $options->getNoCtime()) {
$metadata['ctime'] = filectime($file);
}
if (! $options->getNoAtime()) {
$metadata['atime'] = fileatime($file);
}
$result[$normalizedKey] = $metadata;
}
return $result;
}
/* writing */
/**
* Store an item.
*
* @param string $key
* @param mixed $value
* @return bool
* @throws Exception\ExceptionInterface
*
* @triggers setItem.pre(PreEvent)
* @triggers setItem.post(PostEvent)
* @triggers setItem.exception(ExceptionEvent)
*/
public function setItem($key, $value)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::setItem($key, $value);
}
/**
* Store multiple items.
*
* @param array $keyValuePairs
* @return array Array of not stored keys
* @throws Exception\ExceptionInterface
*
* @triggers setItems.pre(PreEvent)
* @triggers setItems.post(PostEvent)
* @triggers setItems.exception(ExceptionEvent)
*/
public function setItems(array $keyValuePairs)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::setItems($keyValuePairs);
}
/**
* Add an item.
*
* @param string $key
* @param mixed $value
* @return bool
* @throws Exception\ExceptionInterface
*
* @triggers addItem.pre(PreEvent)
* @triggers addItem.post(PostEvent)
* @triggers addItem.exception(ExceptionEvent)
*/
public function addItem($key, $value)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::addItem($key, $value);
}
/**
* Add multiple items.
*
* @param array $keyValuePairs
* @return bool
* @throws Exception\ExceptionInterface
*
* @triggers addItems.pre(PreEvent)
* @triggers addItems.post(PostEvent)
* @triggers addItems.exception(ExceptionEvent)
*/
public function addItems(array $keyValuePairs)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::addItems($keyValuePairs);
}
/**
* Replace an existing item.
*
* @param string $key
* @param mixed $value
* @return bool
* @throws Exception\ExceptionInterface
*
* @triggers replaceItem.pre(PreEvent)
* @triggers replaceItem.post(PostEvent)
* @triggers replaceItem.exception(ExceptionEvent)
*/
public function replaceItem($key, $value)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::replaceItem($key, $value);
}
/**
* Replace multiple existing items.
*
* @param array $keyValuePairs
* @return bool
* @throws Exception\ExceptionInterface
*
* @triggers replaceItems.pre(PreEvent)
* @triggers replaceItems.post(PostEvent)
* @triggers replaceItems.exception(ExceptionEvent)
*/
public function replaceItems(array $keyValuePairs)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::replaceItems($keyValuePairs);
}
/**
* Internal method to store an item.
*
* @param string $normalizedKey
* @param mixed $value
* @return bool
* @throws Exception\ExceptionInterface
*/
protected function internalSetItem(& $normalizedKey, & $value)
{
$filespec = $this->getFileSpec($normalizedKey);
$file = $this->formatFilename($filespec);
$this->prepareDirectoryStructure($filespec);
// write data in non-blocking mode
$wouldblock = null;
$this->putFileContent($file, $value, true, $wouldblock);
// delete related tag file (if present)
$this->unlink($this->formatTagFilename($filespec));
// Retry writing data in blocking mode if it was blocked before
if ($wouldblock) {
$this->putFileContent($file, $value);
}
return true;
}
/**
* Internal method to store multiple items.
*
* @param array $normalizedKeyValuePairs
* @return array Array of not stored keys
* @throws Exception\ExceptionInterface
*/
protected function internalSetItems(array & $normalizedKeyValuePairs)
{
// create an associated array of files and contents to write
$contents = [];
foreach ($normalizedKeyValuePairs as $key => & $value) {
$filespec = $this->getFileSpec($key);
$this->prepareDirectoryStructure($filespec);
// *.dat file
$contents[$this->formatFilename($filespec)] = & $value;
// *.tag file
$this->unlink($this->formatTagFilename($filespec));
}
// write to disk
while ($contents) {
$nonBlocking = count($contents) > 1;
$wouldblock = null;
foreach ($contents as $file => & $content) {
$this->putFileContent($file, $content, $nonBlocking, $wouldblock);
if (! $nonBlocking || ! $wouldblock) {
unset($contents[$file]);
}
}
}
// return OK
return [];
}
/**
* Set an item only if token matches
*
* It uses the token received from getItem() to check if the item has
* changed before overwriting it.
*
* @param mixed $token
* @param string $key
* @param mixed $value
* @return bool
* @throws Exception\ExceptionInterface
* @see getItem()
* @see setItem()
*/
public function checkAndSetItem($token, $key, $value)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::checkAndSetItem($token, $key, $value);
}
/**
* Internal method to set an item only if token matches
*
* @param mixed $token
* @param string $normalizedKey
* @param mixed $value
* @return bool
* @throws Exception\ExceptionInterface
* @see getItem()
* @see setItem()
*/
protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value)
{
if (! $this->internalHasItem($normalizedKey)) {
return false;
}
// use filemtime + filesize as CAS token
$file = $this->formatFilename($this->getFileSpec($normalizedKey));
$check = filemtime($file) . filesize($file);
if ($token !== $check) {
return false;
}
return $this->internalSetItem($normalizedKey, $value);
}
/**
* Reset lifetime of an item
*
* @param string $key
* @return bool
* @throws Exception\ExceptionInterface
*
* @triggers touchItem.pre(PreEvent)
* @triggers touchItem.post(PostEvent)
* @triggers touchItem.exception(ExceptionEvent)
*/
public function touchItem($key)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::touchItem($key);
}
/**
* Reset lifetime of multiple items.
*
* @param array $keys
* @return array Array of not updated keys
* @throws Exception\ExceptionInterface
*
* @triggers touchItems.pre(PreEvent)
* @triggers touchItems.post(PostEvent)
* @triggers touchItems.exception(ExceptionEvent)
*/
public function touchItems(array $keys)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::touchItems($keys);
}
/**
* Internal method to reset lifetime of an item
*
* @param string $normalizedKey
* @return bool
* @throws Exception\ExceptionInterface
*/
protected function internalTouchItem(& $normalizedKey)
{
if (! $this->internalHasItem($normalizedKey)) {
return false;
}
$filespec = $this->getFileSpec($normalizedKey);
$file = $this->formatFilename($filespec);
ErrorHandler::start();
$touch = touch($file);
$error = ErrorHandler::stop();
if (! $touch) {
throw new Exception\RuntimeException("Error touching file '{$file}'", 0, $error);
}
return true;
}
/**
* Remove an item.
*
* @param string $key
* @return bool
* @throws Exception\ExceptionInterface
*
* @triggers removeItem.pre(PreEvent)
* @triggers removeItem.post(PostEvent)
* @triggers removeItem.exception(ExceptionEvent)
*/
public function removeItem($key)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::removeItem($key);
}
/**
* Remove multiple items.
*
* @param array $keys
* @return array Array of not removed keys
* @throws Exception\ExceptionInterface
*
* @triggers removeItems.pre(PreEvent)
* @triggers removeItems.post(PostEvent)
* @triggers removeItems.exception(ExceptionEvent)
*/
public function removeItems(array $keys)
{
$options = $this->getOptions();
if ($options->getWritable() && $options->getClearStatCache()) {
clearstatcache();
}
return parent::removeItems($keys);
}
/**
* Internal method to remove an item.
*
* @param string $normalizedKey
* @return bool
* @throws Exception\ExceptionInterface
*/
protected function internalRemoveItem(& $normalizedKey)
{
$filespec = $this->getFileSpec($normalizedKey);
$file = $this->formatFilename($filespec);
if (! file_exists($file)) {
return false;
}
$this->unlink($file);
$this->unlink($this->formatTagFilename($filespec));
return true;
}
/* status */
/**
* Internal method to get capabilities of this adapter
*
* @return Capabilities
*/
protected function internalGetCapabilities()
{
if ($this->capabilities === null) {
$marker = new stdClass();
$options = $this->getOptions();
// detect metadata
$metadata = ['mtime', 'filespec'];
if (! $options->getNoAtime()) {
$metadata[] = 'atime';
}
if (! $options->getNoCtime()) {
$metadata[] = 'ctime';
}
// Calculate max key length: 255 - strlen(.dat | .tag)
$maxKeyLength = 254 - max([
strlen($this->getOptions()->getSuffix()),
strlen($this->getOptions()->getTagSuffix()),
]);
$capabilities = new Capabilities(
$this,
$marker,
[
'supportedDatatypes' => [
'NULL' => 'string',
'boolean' => 'string',
'integer' => 'string',
'double' => 'string',
'string' => true,
'array' => false,
'object' => false,
'resource' => false,
],
'supportedMetadata' => $metadata,
'minTtl' => 1,
'maxTtl' => 0,
'staticTtl' => false,
'ttlPrecision' => 1,
'maxKeyLength' => $maxKeyLength,
'namespaceIsPrefix' => true,
'namespaceSeparator' => $options->getNamespaceSeparator(),
]
);
// update capabilities on change options
$this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) {
$params = $event->getParams();
if (isset($params['namespace_separator'])) {
$capabilities->setNamespaceSeparator($marker, $params['namespace_separator']);
}
if (isset($params['no_atime']) || isset($params['no_ctime'])) {
$metadata = $capabilities->getSupportedMetadata();
if (isset($params['no_atime']) && ! $params['no_atime']) {
$metadata[] = 'atime';
} elseif (isset($params['no_atime']) && ($index = array_search('atime', $metadata)) !== false) {
unset($metadata[$index]);
}
if (isset($params['no_ctime']) && ! $params['no_ctime']) {
$metadata[] = 'ctime';
} elseif (isset($params['no_ctime']) && ($index = array_search('ctime', $metadata)) !== false) {
unset($metadata[$index]);
}
$capabilities->setSupportedMetadata($marker, $metadata);
}
});
$this->capabilityMarker = $marker;
$this->capabilities = $capabilities;
}
return $this->capabilities;
}
/* internal */
/**
* Removes directories recursive by namespace
*
* @param string $dir Directory to delete
* @param string $prefix Namespace + Separator
* @return bool
*/
protected function rmDir($dir, $prefix)
{
$glob = glob(
$dir . DIRECTORY_SEPARATOR . $prefix . '*',
GLOB_ONLYDIR | GLOB_NOESCAPE | GLOB_NOSORT
);
if (! $glob) {
// On some systems glob returns false even on empty result
return true;
}
$ret = true;
foreach ($glob as $subdir) {
// skip removing current directory if removing of sub-directory failed
if ($this->rmDir($subdir, $prefix)) {
// ignore not empty directories
ErrorHandler::start();
$ret = rmdir($subdir) && $ret;
ErrorHandler::stop();
} else {
$ret = false;
}
}
return $ret;
}
/**
* Get file spec of the given key and namespace
*
* @param string $normalizedKey
* @return string
*/
protected function getFileSpec($normalizedKey)
{
$options = $this->getOptions();
$namespace = $options->getNamespace();
$prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator();
$path = $options->getCacheDir() . DIRECTORY_SEPARATOR;
$level = $options->getDirLevel();
$fileSpecId = $path . $prefix . $normalizedKey . '/' . $level;
if ($this->lastFileSpecId !== $fileSpecId) {
if ($level > 0) {
// create up to 256 directories per directory level
$hash = md5($normalizedKey);
for ($i = 0, $max = ($level * 2); $i < $max; $i += 2) {
$path .= $prefix . $hash[$i] . $hash[$i + 1] . DIRECTORY_SEPARATOR;
}
}
$this->lastFileSpecId = $fileSpecId;
$this->lastFileSpec = $path . $prefix . $normalizedKey;
}
return $this->lastFileSpec;
}
/**
* Read a complete file
*
* @param string $file File complete path
* @param bool $nonBlocking Don't block script if file is locked
* @param bool $wouldblock The optional argument is set to TRUE if the lock would block
* @return string
* @throws Exception\RuntimeException
*/
protected function getFileContent($file, $nonBlocking = false, & $wouldblock = null)
{
$locking = $this->getOptions()->getFileLocking();
$wouldblock = null;
ErrorHandler::start();
// if file locking enabled -> file_get_contents can't be used
if ($locking) {
$fp = fopen($file, 'rb');
if ($fp === false) {
$err = ErrorHandler::stop();
throw new Exception\RuntimeException("Error opening file '{$file}'", 0, $err);
}
if ($nonBlocking) {
$lock = flock($fp, LOCK_SH | LOCK_NB, $wouldblock);
if ($wouldblock) {
fclose($fp);
ErrorHandler::stop();
return;
}
} else {
$lock = flock($fp, LOCK_SH);
}
if (! $lock) {
fclose($fp);
$err = ErrorHandler::stop();
throw new Exception\RuntimeException("Error locking file '{$file}'", 0, $err);
}
$res = stream_get_contents($fp);
if ($res === false) {
flock($fp, LOCK_UN);
fclose($fp);
$err = ErrorHandler::stop();
throw new Exception\RuntimeException('Error getting stream contents', 0, $err);
}
flock($fp, LOCK_UN);
fclose($fp);
// if file locking disabled -> file_get_contents can be used
} else {
$res = file_get_contents($file, false);
if ($res === false) {
$err = ErrorHandler::stop();
throw new Exception\RuntimeException("Error getting file contents for file '{$file}'", 0, $err);
}
}
ErrorHandler::stop();
return $res;
}
/**
* Prepares a directory structure for the given file(spec)
* using the configured directory level.
*
* @param string $file
* @return void
* @throws Exception\RuntimeException
*/
protected function prepareDirectoryStructure($file)
{
$options = $this->getOptions();
$level = $options->getDirLevel();
// Directory structure is required only if directory level > 0
if (! $level) {
return;
}
// Directory structure already exists
$pathname = dirname($file);
if (file_exists($pathname)) {
return;
}
$perm = $options->getDirPermission();
$umask = $options->getUmask();
if ($umask !== false && $perm !== false) {
$perm = $perm & ~$umask;
}
ErrorHandler::start();
if ($perm === false || $level == 1) {
// built-in mkdir function is enough
$umask = ($umask !== false) ? umask($umask) : false;
$res = mkdir($pathname, ($perm !== false) ? $perm : 0775, true);
if ($umask !== false) {
umask($umask);
}
if (! $res) {
$err = ErrorHandler::stop();
// Issue 6435:
// mkdir could fail because of a race condition it was already created by another process
// after the first file_exists above
if (file_exists($pathname)) {
return;
}
$oct = ($perm === false) ? '775' : decoct($perm);
throw new Exception\RuntimeException("mkdir('{$pathname}', 0{$oct}, true) failed", 0, $err);
}
if ($perm !== false && ! chmod($pathname, $perm)) {
$oct = decoct($perm);
$err = ErrorHandler::stop();
throw new Exception\RuntimeException("chmod('{$pathname}', 0{$oct}) failed", 0, $err);
}
} else {
// built-in mkdir function sets permission together with current umask
// which doesn't work well on multo threaded webservers
// -> create directories one by one and set permissions
// find existing path and missing path parts
$parts = [];
$path = $pathname;
while (! file_exists($path)) {
array_unshift($parts, basename($path));
$nextPath = dirname($path);
if ($nextPath === $path) {
break;
}
$path = $nextPath;
}
// make all missing path parts
foreach ($parts as $part) {
$path .= DIRECTORY_SEPARATOR . $part;
// create a single directory, set and reset umask immediately
$umask = ($umask !== false) ? umask($umask) : false;
$res = mkdir($path, ($perm === false) ? 0775 : $perm, false);
if ($umask !== false) {
umask($umask);
}
if (! $res) {
// Issue 6435:
// mkdir could fail because of a race condition it was already created by another process
// after the first file_exists above ... go to the next path part.
if (file_exists($path)) {
continue;
}
$oct = ($perm === false) ? '775' : decoct($perm);
ErrorHandler::stop();
throw new Exception\RuntimeException(
"mkdir('{$path}', 0{$oct}, false) failed"
);
}
if ($perm !== false && ! chmod($path, $perm)) {
$oct = decoct($perm);
ErrorHandler::stop();
throw new Exception\RuntimeException(
"chmod('{$path}', 0{$oct}) failed"
);
}
}
}
ErrorHandler::stop();
}
/**
* Write content to a file
*
* @param string $file File complete path
* @param string $data Data to write
* @param bool $nonBlocking Don't block script if file is locked
* @param bool $wouldblock The optional argument is set to TRUE if the lock would block
* @return void
* @throws Exception\RuntimeException
*/
protected function putFileContent($file, $data, $nonBlocking = false, & $wouldblock = null)
{
if (! is_string($data)) {
// Ensure we have a string
$data = (string) $data;
}
$options = $this->getOptions();
$locking = $options->getFileLocking();
$nonBlocking = $locking && $nonBlocking;
$wouldblock = null;
$umask = $options->getUmask();
$perm = $options->getFilePermission();
if ($umask !== false && $perm !== false) {
$perm = $perm & ~$umask;
}
ErrorHandler::start();
// if locking and non blocking is enabled -> file_put_contents can't used
if ($locking && $nonBlocking) {
$umask = ($umask !== false) ? umask($umask) : false;
$fp = fopen($file, 'cb');
if ($umask) {
umask($umask);
}
if (! $fp) {
$err = ErrorHandler::stop();
throw new Exception\RuntimeException("Error opening file '{$file}'", 0, $err);
}
if ($perm !== false && ! chmod($file, $perm)) {
fclose($fp);
$oct = decoct($perm);
$err = ErrorHandler::stop();
throw new Exception\RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err);
}
if (! flock($fp, LOCK_EX | LOCK_NB, $wouldblock)) {
fclose($fp);
$err = ErrorHandler::stop();
if ($wouldblock) {
return;
} else {
throw new Exception\RuntimeException("Error locking file '{$file}'", 0, $err);
}
}
if (fwrite($fp, $data) === false) {
flock($fp, LOCK_UN);
fclose($fp);
$err = ErrorHandler::stop();
throw new Exception\RuntimeException("Error writing file '{$file}'", 0, $err);
}
if (! ftruncate($fp, strlen($data))) {
flock($fp, LOCK_UN);
fclose($fp);
$err = ErrorHandler::stop();
throw new Exception\RuntimeException("Error truncating file '{$file}'", 0, $err);
}
flock($fp, LOCK_UN);
fclose($fp);
// else -> file_put_contents can be used
} else {
$flags = 0;
if ($locking) {
$flags = $flags | LOCK_EX;
}
$umask = ($umask !== false) ? umask($umask) : false;
$rs = file_put_contents($file, $data, $flags);
if ($umask) {
umask($umask);
}
if ($rs === false) {
$err = ErrorHandler::stop();
throw new Exception\RuntimeException("Error writing file '{$file}'", 0, $err);
}
if ($perm !== false && ! chmod($file, $perm)) {
$oct = decoct($perm);
$err = ErrorHandler::stop();
throw new Exception\RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err);
}
}
ErrorHandler::stop();
}
/**
* Unlink a file
*
* @param string $file
* @return void
* @throws Exception\RuntimeException
*/
protected function unlink($file)
{
ErrorHandler::start();
$res = unlink($file);
$err = ErrorHandler::stop();
// only throw exception if file still exists after deleting
if (! $res && file_exists($file)) {
throw new Exception\RuntimeException(
"Error unlinking file '{$file}'; file still exists",
0,
$err
);
}
}
/**
* Formats the filename, appending the suffix option
*
* @param string $filename
* @return string
*/
private function formatFilename($filename)
{
return sprintf('%s.%s', $filename, $this->getOptions()->getSuffix());
}
/**
* Formats the filename, appending the tag suffix option
*
* @param string $filename
* @return string
*/
private function formatTagFilename($filename)
{
return sprintf('%s.%s', $filename, $this->getOptions()->getTagSuffix());
}
/**
* Escapes a filename suffix to be safe for glob operations
*
* Wraps any of *, ?, or [ characters within [] brackets.
*
* @param string $suffix
* @return string
*/
private function escapeSuffixForGlob($suffix)
{
return preg_replace('#([*?\[])#', '[$1]', $suffix);
}
}
You can’t perform that action at this time.