Source of file Filesystem.php
Size: 47,422 Bytes - Last Modified: 2014-03-12T23:21:18+01:00
/home/theseer/Downloads/ZendFramework-2.3.0/library/Zend/Cache/Storage/Adapter/Filesystem.php
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618 | <?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace Zend\Cache\Storage\Adapter; use Exception as BaseException; use GlobIterator; use stdClass; use Zend\Cache\Exception; use Zend\Cache\Storage; use Zend\Cache\Storage\AvailableSpaceCapableInterface; use Zend\Cache\Storage\Capabilities; use Zend\Cache\Storage\ClearByNamespaceInterface; use Zend\Cache\Storage\ClearByPrefixInterface; use Zend\Cache\Storage\ClearExpiredInterface; use Zend\Cache\Storage\FlushableInterface; use Zend\Cache\Storage\IterableInterface; use Zend\Cache\Storage\OptimizableInterface; use Zend\Cache\Storage\TaggableInterface; use Zend\Cache\Storage\TotalSpaceCapableInterface; use Zend\Stdlib\ErrorHandler; 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 { unlink($pathname); } } }; 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 */ public function clearExpired() { $options = $this->getOptions(); $namespace = $options->getNamespace(); $prefix = ($namespace === '') ? '' : $namespace . $options->getNamespaceSeparator(); $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_FILEINFO; $path = $options->getCacheDir() . str_repeat(DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel()) . DIRECTORY_SEPARATOR . $prefix . '*.dat'; $glob = new GlobIterator($path, $flags); $time = time(); $ttl = $options->getTtl(); ErrorHandler::start(); foreach ($glob as $entry) { $mtime = $entry->getMTime(); if ($time >= $mtime + $ttl) { $pathname = $entry->getPathname(); unlink($pathname); $tagPathname = substr($pathname, 0, -4) . '.tag'; if (file_exists($tagPathname)) { unlink($tagPathname); } } } $error = ErrorHandler::stop(); if ($error) { throw 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) { unlink($pathname); } $error = ErrorHandler::stop(); if ($error) { throw new Exception\RuntimeException("Failed to remove files of '{$path}'", 0, $error); } 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) { unlink($pathname); } $error = ErrorHandler::stop(); if ($error) { throw new Exception\RuntimeException("Failed to remove files of '{$path}'", 0, $error); } 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($filespec . '.tag'); return true; } $this->putFileContent($filespec . '.tag', 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->getFileSpec($key); $tags = array(); if (file_exists($filespec . '.tag')) { $tags = explode("\n", $this->getFileContent($filespec . '.tag')); } 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 . '*.tag'; $glob = new GlobIterator($path, $flags); foreach ($glob as $pathname) { $diff = array_diff($tags, explode("\n", $this->getFileContent($pathname))); $rem = false; if ($disjunction && count($diff) < $tagCount) { $rem = true; } elseif (!$disjunction && !$diff) { $rem = true; } if ($rem) { unlink($pathname); $datPathname = substr($pathname, 0, -4) . '.dat'; 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 . '*.dat'; return new FilesystemIterator($this, $path, $prefix); } /* OptimizableInterface */ /** * Optimize the storage * * @return bool * @return 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 int|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 mixed Data on success, null on failure * @throws Exception\ExceptionInterface */ protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null) { if (!$this->internalHasItem($normalizedKey)) { $success = false; return null; } try { $filespec = $this->getFileSpec($normalizedKey); $data = $this->getFileContent($filespec . '.dat'); // use filemtime + filesize as CAS token if (func_num_args() > 2) { $casToken = filemtime($filespec . '.dat') . filesize($filespec . '.dat'); } $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 = array(); 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->getFileSpec($key); $data = $this->getFileContent($filespec . '.dat', $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->getFileSpec($normalizedKey) . '.dat'; 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 = array()) { $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 = $filespec . '.dat'; $metadata = array( '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 = array(); foreach ($normalizedKeys as $normalizedKey) { $filespec = $this->getFileSpec($normalizedKey); $file = $filespec . '.dat'; $metadata = array( '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); $this->prepareDirectoryStructure($filespec); // write data in non-blocking mode $wouldblock = null; $this->putFileContent($filespec . '.dat', $value, true, $wouldblock); // delete related tag file (if present) $this->unlink($filespec . '.tag'); // Retry writing data in blocking mode if it was blocked before if ($wouldblock) { $this->putFileContent($filespec . '.dat', $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) { $oldUmask = null; // create an associated array of files and contents to write $contents = array(); foreach ($normalizedKeyValuePairs as $key => & $value) { $filespec = $this->getFileSpec($key); $this->prepareDirectoryStructure($filespec); // *.dat file $contents[$filespec . '.dat'] = & $value; // *.tag file $this->unlink($filespec . '.tag'); } // 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 array(); } /** * 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->getFileSpec($normalizedKey) . '.dat'; $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); ErrorHandler::start(); $touch = touch($filespec . '.dat'); $error = ErrorHandler::stop(); if (!$touch) { throw new Exception\RuntimeException( "Error touching file '{$filespec}.dat'", 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); if (!file_exists($filespec . '.dat')) { return false; } else { $this->unlink($filespec . '.dat'); $this->unlink($filespec . '.tag'); } 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 = array('mtime', 'filespec'); if (!$options->getNoAtime()) { $metadata[] = 'atime'; } if (!$options->getNoCtime()) { $metadata[] = 'ctime'; } $capabilities = new Capabilities( $this, $marker, array( 'supportedDatatypes' => array( '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, 'expiredRead' => true, 'maxKeyLength' => 251, // 255 - strlen(.dat | .tag) '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 info file * * @param string $file * @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 array|bool The info array or false if file wasn't found * @throws Exception\RuntimeException */ protected function readInfoFile($file, $nonBlocking = false, & $wouldblock = null) { if (!file_exists($file)) { return false; } $content = $this->getFileContent($file, $nonBlocking, $wouldblock); if ($nonBlocking && $wouldblock) { return false; } ErrorHandler::start(); $ifo = unserialize($content); $err = ErrorHandler::stop(); if (!is_array($ifo)) { throw new Exception\RuntimeException( "Corrupted info file '{$file}'", 0, $err ); } return $ifo; } /** * 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) { // build-in mkdir function is enough $umask = ($umask !== false) ? umask($umask) : false; $res = mkdir($pathname, ($perm !== false) ? $perm : 0777, true); if ($umask !== false) { umask($umask); } if (!$res) { $oct = ($perm === false) ? '777' : decoct($perm); $err = ErrorHandler::stop(); 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 { // build-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 = array(); $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) ? 0777 : $perm, false); if ($umask !== false) { umask($umask); } if (!$res) { $oct = ($perm === false) ? '777' : 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) { $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 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 ); } } } |