PageRenderTime 44ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/vendor/zendframework/zendframework/library/Zend/Cache/Storage/Adapter/Filesystem.php

https://bitbucket.org/pcelta/zf2
PHP | 1600 lines | 919 code | 206 blank | 475 comment | 157 complexity | 6b5ee754674e151ed0044f37b2eb029c MD5 | raw file
  1. <?php
  2. /**
  3. * Zend Framework (http://framework.zend.com/)
  4. *
  5. * @link http://github.com/zendframework/zf2 for the canonical source repository
  6. * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
  7. * @license http://framework.zend.com/license/new-bsd New BSD License
  8. * @package Zend_Cache
  9. */
  10. namespace Zend\Cache\Storage\Adapter;
  11. use Exception as BaseException;
  12. use GlobIterator;
  13. use stdClass;
  14. use Zend\Cache\Exception;
  15. use Zend\Cache\Storage;
  16. use Zend\Cache\Storage\AvailableSpaceCapableInterface;
  17. use Zend\Cache\Storage\Capabilities;
  18. use Zend\Cache\Storage\ClearByNamespaceInterface;
  19. use Zend\Cache\Storage\ClearByPrefixInterface;
  20. use Zend\Cache\Storage\ClearExpiredInterface;
  21. use Zend\Cache\Storage\FlushableInterface;
  22. use Zend\Cache\Storage\IterableInterface;
  23. use Zend\Cache\Storage\OptimizableInterface;
  24. use Zend\Cache\Storage\TaggableInterface;
  25. use Zend\Cache\Storage\TotalSpaceCapableInterface;
  26. use Zend\Stdlib\ErrorHandler;
  27. /**
  28. * @category Zend
  29. * @package Zend_Cache
  30. * @subpackage Storage
  31. */
  32. class Filesystem extends AbstractAdapter implements
  33. AvailableSpaceCapableInterface,
  34. ClearByNamespaceInterface,
  35. ClearByPrefixInterface,
  36. ClearExpiredInterface,
  37. FlushableInterface,
  38. IterableInterface,
  39. OptimizableInterface,
  40. TaggableInterface,
  41. TotalSpaceCapableInterface
  42. {
  43. /**
  44. * Buffered total space in bytes
  45. *
  46. * @var null|int|float
  47. */
  48. protected $totalSpace;
  49. /**
  50. * An identity for the last filespec
  51. * (cache directory + namespace prefix + key + directory level)
  52. *
  53. * @var string
  54. */
  55. protected $lastFileSpecId = '';
  56. /**
  57. * The last used filespec
  58. *
  59. * @var string
  60. */
  61. protected $lastFileSpec = '';
  62. /**
  63. * Set options.
  64. *
  65. * @param array|\Traversable|FilesystemOptions $options
  66. * @return Filesystem
  67. * @see getOptions()
  68. */
  69. public function setOptions($options)
  70. {
  71. if (!$options instanceof FilesystemOptions) {
  72. $options = new FilesystemOptions($options);
  73. }
  74. return parent::setOptions($options);
  75. }
  76. /**
  77. * Get options.
  78. *
  79. * @return FilesystemOptions
  80. * @see setOptions()
  81. */
  82. public function getOptions()
  83. {
  84. if (!$this->options) {
  85. $this->setOptions(new FilesystemOptions());
  86. }
  87. return $this->options;
  88. }
  89. /* FlushableInterface */
  90. /**
  91. * Flush the whole storage
  92. *
  93. * @throws Exception\RuntimeException
  94. * @return boolean
  95. */
  96. public function flush()
  97. {
  98. $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
  99. $dir = $this->getOptions()->getCacheDir();
  100. $clearFolder = null;
  101. $clearFolder = function ($dir) use (& $clearFolder, $flags) {
  102. $it = new GlobIterator($dir . \DIRECTORY_SEPARATOR . '*', $flags);
  103. foreach ($it as $pathname) {
  104. if ($it->isDir()) {
  105. $clearFolder($pathname);
  106. rmdir($pathname);
  107. } else {
  108. unlink($pathname);
  109. }
  110. }
  111. };
  112. ErrorHandler::start();
  113. $clearFolder($dir);
  114. $error = ErrorHandler::stop();
  115. if ($error) {
  116. throw new Exception\RuntimeException("Flushing directory '{$dir}' failed", 0, $error);
  117. }
  118. return true;
  119. }
  120. /* ClearExpiredInterface */
  121. /**
  122. * Remove expired items
  123. *
  124. * @return boolean
  125. */
  126. public function clearExpired()
  127. {
  128. $options = $this->getOptions();
  129. $prefix = $options->getNamespace() . $options->getNamespaceSeparator();
  130. $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_FILEINFO;
  131. $path = $options->getCacheDir()
  132. . str_repeat(\DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel())
  133. . \DIRECTORY_SEPARATOR . $prefix . '*.dat';
  134. $glob = new GlobIterator($path, $flags);
  135. $time = time();
  136. $ttl = $options->getTtl();
  137. ErrorHandler::start();
  138. foreach ($glob as $entry) {
  139. $mtime = $entry->getMTime();
  140. if ($time >= $mtime + $ttl) {
  141. $pathname = $entry->getPathname();
  142. unlink($pathname);
  143. $tagPathname = substr($pathname, 0, -4) . '.tag';
  144. if (file_exists($tagPathname)) {
  145. unlink($tagPathname);
  146. }
  147. }
  148. }
  149. $error = ErrorHandler::stop();
  150. if ($error) {
  151. throw new Exception\RuntimeException("Failed to clear expired items", 0, $error);
  152. }
  153. return true;
  154. }
  155. /* ClearByNamespaceInterface */
  156. /**
  157. * Remove items by given namespace
  158. *
  159. * @param string $namespace
  160. * @throws Exception\RuntimeException
  161. * @return boolean
  162. */
  163. public function clearByNamespace($namespace)
  164. {
  165. $options = $this->getOptions();
  166. $nsPrefix = $namespace . $options->getNamespaceSeparator();
  167. $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
  168. $path = $options->getCacheDir()
  169. . str_repeat(\DIRECTORY_SEPARATOR . $nsPrefix . '*', $options->getDirLevel())
  170. . \DIRECTORY_SEPARATOR . $nsPrefix . '*';
  171. $glob = new GlobIterator($path, $flags);
  172. ErrorHandler::start();
  173. foreach ($glob as $pathname) {
  174. unlink($pathname);
  175. }
  176. $error = ErrorHandler::stop();
  177. if ($error) {
  178. throw new Exception\RuntimeException("Failed to remove file '{$pathname}'", 0, $error);
  179. }
  180. return true;
  181. }
  182. /* ClearByPrefixInterface */
  183. /**
  184. * Remove items matching given prefix
  185. *
  186. * @param string $prefix
  187. * @throws Exception\RuntimeException
  188. * @return boolean
  189. */
  190. public function clearByPrefix($prefix)
  191. {
  192. $options = $this->getOptions();
  193. $nsPrefix = $options->getNamespace() . $options->getNamespaceSeparator();
  194. $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
  195. $path = $options->getCacheDir()
  196. . str_repeat(\DIRECTORY_SEPARATOR . $nsPrefix . '*', $options->getDirLevel())
  197. . \DIRECTORY_SEPARATOR . $nsPrefix . $prefix . '*';
  198. $glob = new GlobIterator($path, $flags);
  199. ErrorHandler::start();
  200. foreach ($glob as $pathname) {
  201. unlink($pathname);
  202. }
  203. $error = ErrorHandler::stop();
  204. if ($error) {
  205. throw new Exception\RuntimeException("Failed to remove file '{$pathname}'", 0, $error);
  206. }
  207. return true;
  208. }
  209. /* TaggableInterface */
  210. /**
  211. * Set tags to an item by given key.
  212. * An empty array will remove all tags.
  213. *
  214. * @param string $key
  215. * @param string[] $tags
  216. * @return boolean
  217. */
  218. public function setTags($key, array $tags)
  219. {
  220. $this->normalizeKey($key);
  221. if (!$this->internalHasItem($key)) {
  222. return false;
  223. }
  224. $filespec = $this->getFileSpec($key);
  225. if (!$tags) {
  226. $this->unlink($filespec . '.tag');
  227. return true;
  228. }
  229. $this->putFileContent($filespec . '.tag', implode("\n", $tags));
  230. return true;
  231. }
  232. /**
  233. * Get tags of an item by given key
  234. *
  235. * @param string $key
  236. * @return string[]|FALSE
  237. */
  238. public function getTags($key)
  239. {
  240. $this->normalizeKey($key);
  241. if (!$this->internalHasItem($key)) {
  242. return false;
  243. }
  244. $filespec = $this->getFileSpec($key);
  245. $tags = array();
  246. if (file_exists($filespec . '.tag')) {
  247. $tags = explode("\n", $this->getFileContent($filespec . '.tag'));
  248. }
  249. return $tags;
  250. }
  251. /**
  252. * Remove items matching given tags.
  253. *
  254. * If $disjunction only one of the given tags must match
  255. * else all given tags must match.
  256. *
  257. * @param string[] $tags
  258. * @param boolean $disjunction
  259. * @return boolean
  260. */
  261. public function clearByTags(array $tags, $disjunction = false)
  262. {
  263. if (!$tags) {
  264. return true;
  265. }
  266. $tagCount = count($tags);
  267. $options = $this->getOptions();
  268. $prefix = $options->getNamespace() . $options->getNamespaceSeparator();
  269. $flags = GlobIterator::SKIP_DOTS | GlobIterator::CURRENT_AS_PATHNAME;
  270. $path = $options->getCacheDir()
  271. . str_repeat(\DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel())
  272. . \DIRECTORY_SEPARATOR . $prefix . '*.tag';
  273. $glob = new GlobIterator($path, $flags);
  274. foreach ($glob as $pathname) {
  275. $diff = array_diff($tags, explode("\n", $this->getFileContent($pathname)));
  276. $rem = false;
  277. if ($disjunction && count($diff) < $tagCount) {
  278. $rem = true;
  279. } elseif (!$disjunction && !$diff) {
  280. $rem = true;
  281. }
  282. if ($rem) {
  283. unlink($pathname);
  284. $datPathname = substr($pathname, 0, -4) . '.dat';
  285. if (file_exists($datPathname)) {
  286. unlink($datPathname);
  287. }
  288. }
  289. }
  290. return true;
  291. }
  292. /* IterableInterface */
  293. /**
  294. * Get the storage iterator
  295. *
  296. * @return FilesystemIterator
  297. */
  298. public function getIterator()
  299. {
  300. $options = $this->getOptions();
  301. $prefix = $options->getNamespace() . $options->getNamespaceSeparator();
  302. $path = $options->getCacheDir()
  303. . str_repeat(\DIRECTORY_SEPARATOR . $prefix . '*', $options->getDirLevel())
  304. . \DIRECTORY_SEPARATOR . $prefix . '*.dat';
  305. return new FilesystemIterator($this, $path, $prefix);
  306. }
  307. /* OptimizableInterface */
  308. /**
  309. * Optimize the storage
  310. *
  311. * @return boolean
  312. * @return Exception\RuntimeException
  313. */
  314. public function optimize()
  315. {
  316. $baseOptions = $this->getOptions();
  317. if ($baseOptions->getDirLevel()) {
  318. // removes only empty directories
  319. $this->rmDir(
  320. $baseOptions->getCacheDir(),
  321. $baseOptions->getNamespace() . $baseOptions->getNamespaceSeparator()
  322. );
  323. }
  324. return true;
  325. }
  326. /* TotalSpaceCapableInterface */
  327. /**
  328. * Get total space in bytes
  329. *
  330. * @throws Exception\RuntimeException
  331. * @return int|float
  332. */
  333. public function getTotalSpace()
  334. {
  335. if ($this->totalSpace === null) {
  336. $path = $this->getOptions()->getCacheDir();
  337. ErrorHandler::start();
  338. $total = disk_total_space($path);
  339. $error = ErrorHandler::stop();
  340. if ($total === false) {
  341. throw new Exception\RuntimeException("Can't detect total space of '{$path}'", 0, $error);
  342. }
  343. $this->totalSpace = $total;
  344. // clean total space buffer on change cache_dir
  345. $events = $this->getEventManager();
  346. $handle = null;
  347. $totalSpace = & $this->totalSpace;
  348. $callback = function ($event) use (& $events, & $handle, & $totalSpace) {
  349. $params = $event->getParams();
  350. if (isset($params['cache_dir'])) {
  351. $totalSpace = null;
  352. $events->detach($handle);
  353. }
  354. };
  355. $handle = $events->attach('option', $callback);
  356. }
  357. return $this->totalSpace;
  358. }
  359. /* AvailableSpaceCapableInterface */
  360. /**
  361. * Get available space in bytes
  362. *
  363. * @throws Exception\RuntimeException
  364. * @return int|float
  365. */
  366. public function getAvailableSpace()
  367. {
  368. $path = $this->getOptions()->getCacheDir();
  369. ErrorHandler::start();
  370. $avail = disk_free_space($path);
  371. $error = ErrorHandler::stop();
  372. if ($avail === false) {
  373. throw new Exception\RuntimeException("Can't detect free space of '{$path}'", 0, $error);
  374. }
  375. return $avail;
  376. }
  377. /* reading */
  378. /**
  379. * Get an item.
  380. *
  381. * @param string $key
  382. * @param boolean $success
  383. * @param mixed $casToken
  384. * @return mixed Data on success, null on failure
  385. * @throws Exception\ExceptionInterface
  386. *
  387. * @triggers getItem.pre(PreEvent)
  388. * @triggers getItem.post(PostEvent)
  389. * @triggers getItem.exception(ExceptionEvent)
  390. */
  391. public function getItem($key, & $success = null, & $casToken = null)
  392. {
  393. $options = $this->getOptions();
  394. if ($options->getReadable() && $options->getClearStatCache()) {
  395. clearstatcache();
  396. }
  397. $argn = func_num_args();
  398. if ($argn > 2) {
  399. return parent::getItem($key, $success, $casToken);
  400. } elseif ($argn > 1) {
  401. return parent::getItem($key, $success);
  402. } else {
  403. return parent::getItem($key);
  404. }
  405. }
  406. /**
  407. * Get multiple items.
  408. *
  409. * @param array $keys
  410. * @return array Associative array of keys and values
  411. * @throws Exception\ExceptionInterface
  412. *
  413. * @triggers getItems.pre(PreEvent)
  414. * @triggers getItems.post(PostEvent)
  415. * @triggers getItems.exception(ExceptionEvent)
  416. */
  417. public function getItems(array $keys)
  418. {
  419. $options = $this->getOptions();
  420. if ($options->getReadable() && $options->getClearStatCache()) {
  421. clearstatcache();
  422. }
  423. return parent::getItems($keys);
  424. }
  425. /**
  426. * Internal method to get an item.
  427. *
  428. * @param string $normalizedKey
  429. * @param boolean $success
  430. * @param mixed $casToken
  431. * @return mixed Data on success, null on failure
  432. * @throws Exception\ExceptionInterface
  433. */
  434. protected function internalGetItem(& $normalizedKey, & $success = null, & $casToken = null)
  435. {
  436. if (!$this->internalHasItem($normalizedKey)) {
  437. $success = false;
  438. return null;
  439. }
  440. try {
  441. $filespec = $this->getFileSpec($normalizedKey);
  442. $data = $this->getFileContent($filespec . '.dat');
  443. // use filemtime + filesize as CAS token
  444. if (func_num_args() > 2) {
  445. $casToken = filemtime($filespec . '.dat') . filesize($filespec . '.dat');
  446. }
  447. $success = true;
  448. return $data;
  449. } catch (BaseException $e) {
  450. $success = false;
  451. throw $e;
  452. }
  453. }
  454. /**
  455. * Internal method to get multiple items.
  456. *
  457. * @param array $normalizedKeys
  458. * @return array Associative array of keys and values
  459. * @throws Exception\ExceptionInterface
  460. */
  461. protected function internalGetItems(array & $normalizedKeys)
  462. {
  463. $options = $this->getOptions();
  464. $keys = $normalizedKeys; // Don't change argument passed by reference
  465. $result = array();
  466. while ($keys) {
  467. // LOCK_NB if more than one items have to read
  468. $nonBlocking = count($keys) > 1;
  469. $wouldblock = null;
  470. // read items
  471. foreach ($keys as $i => $key) {
  472. if (!$this->internalHasItem($key)) {
  473. unset($keys[$i]);
  474. continue;
  475. }
  476. $filespec = $this->getFileSpec($key);
  477. $data = $this->getFileContent($filespec . '.dat', $nonBlocking, $wouldblock);
  478. if ($nonBlocking && $wouldblock) {
  479. continue;
  480. } else {
  481. unset($keys[$i]);
  482. }
  483. $result[$key] = $data;
  484. }
  485. // TODO: Don't check ttl after first iteration
  486. // $options['ttl'] = 0;
  487. }
  488. return $result;
  489. }
  490. /**
  491. * Test if an item exists.
  492. *
  493. * @param string $key
  494. * @return boolean
  495. * @throws Exception\ExceptionInterface
  496. *
  497. * @triggers hasItem.pre(PreEvent)
  498. * @triggers hasItem.post(PostEvent)
  499. * @triggers hasItem.exception(ExceptionEvent)
  500. */
  501. public function hasItem($key)
  502. {
  503. $options = $this->getOptions();
  504. if ($options->getReadable() && $options->getClearStatCache()) {
  505. clearstatcache();
  506. }
  507. return parent::hasItem($key);
  508. }
  509. /**
  510. * Test multiple items.
  511. *
  512. * @param array $keys
  513. * @return array Array of found keys
  514. * @throws Exception\ExceptionInterface
  515. *
  516. * @triggers hasItems.pre(PreEvent)
  517. * @triggers hasItems.post(PostEvent)
  518. * @triggers hasItems.exception(ExceptionEvent)
  519. */
  520. public function hasItems(array $keys)
  521. {
  522. $options = $this->getOptions();
  523. if ($options->getReadable() && $options->getClearStatCache()) {
  524. clearstatcache();
  525. }
  526. return parent::hasItems($keys);
  527. }
  528. /**
  529. * Internal method to test if an item exists.
  530. *
  531. * @param string $normalizedKey
  532. * @return boolean
  533. * @throws Exception\ExceptionInterface
  534. */
  535. protected function internalHasItem(& $normalizedKey)
  536. {
  537. $file = $this->getFileSpec($normalizedKey) . '.dat';
  538. if (!file_exists($file)) {
  539. return false;
  540. }
  541. $ttl = $this->getOptions()->getTtl();
  542. if ($ttl) {
  543. ErrorHandler::start();
  544. $mtime = filemtime($file);
  545. $error = ErrorHandler::stop();
  546. if (!$mtime) {
  547. throw new Exception\RuntimeException(
  548. "Error getting mtime of file '{$file}'", 0, $error
  549. );
  550. }
  551. if (time() >= ($mtime + $ttl)) {
  552. return false;
  553. }
  554. }
  555. return true;
  556. }
  557. /**
  558. * Get metadata
  559. *
  560. * @param string $key
  561. * @return array|boolean Metadata on success, false on failure
  562. */
  563. public function getMetadata($key)
  564. {
  565. $options = $this->getOptions();
  566. if ($options->getReadable() && $options->getClearStatCache()) {
  567. clearstatcache();
  568. }
  569. return parent::getMetadata($key);
  570. }
  571. /**
  572. * Get metadatas
  573. *
  574. * @param array $keys
  575. * @param array $options
  576. * @return array Associative array of keys and metadata
  577. */
  578. public function getMetadatas(array $keys, array $options = array())
  579. {
  580. $options = $this->getOptions();
  581. if ($options->getReadable() && $options->getClearStatCache()) {
  582. clearstatcache();
  583. }
  584. return parent::getMetadatas($keys);
  585. }
  586. /**
  587. * Get info by key
  588. *
  589. * @param string $normalizedKey
  590. * @return array|boolean Metadata on success, false on failure
  591. */
  592. protected function internalGetMetadata(& $normalizedKey)
  593. {
  594. if (!$this->internalHasItem($normalizedKey)) {
  595. return false;
  596. }
  597. $options = $this->getOptions();
  598. $filespec = $this->getFileSpec($normalizedKey);
  599. $file = $filespec . '.dat';
  600. $metadata = array(
  601. 'filespec' => $filespec,
  602. 'mtime' => filemtime($file)
  603. );
  604. if (!$options->getNoCtime()) {
  605. $metadata['ctime'] = filectime($file);
  606. }
  607. if (!$options->getNoAtime()) {
  608. $metadata['atime'] = fileatime($file);
  609. }
  610. return $metadata;
  611. }
  612. /**
  613. * Internal method to get multiple metadata
  614. *
  615. * @param array $normalizedKeys
  616. * @return array Associative array of keys and metadata
  617. * @throws Exception\ExceptionInterface
  618. */
  619. protected function internalGetMetadatas(array & $normalizedKeys)
  620. {
  621. $options = $this->getOptions();
  622. $result = array();
  623. foreach ($normalizedKeys as $normalizedKey) {
  624. $filespec = $this->getFileSpec($normalizedKey);
  625. $file = $filespec . '.dat';
  626. $metadata = array(
  627. 'filespec' => $filespec,
  628. 'mtime' => filemtime($file),
  629. );
  630. if (!$options->getNoCtime()) {
  631. $metadata['ctime'] = filectime($file);
  632. }
  633. if (!$options->getNoAtime()) {
  634. $metadata['atime'] = fileatime($file);
  635. }
  636. $result[$normalizedKey] = $metadata;
  637. }
  638. return $result;
  639. }
  640. /* writing */
  641. /**
  642. * Store an item.
  643. *
  644. * @param string $key
  645. * @param mixed $value
  646. * @return boolean
  647. * @throws Exception\ExceptionInterface
  648. *
  649. * @triggers setItem.pre(PreEvent)
  650. * @triggers setItem.post(PostEvent)
  651. * @triggers setItem.exception(ExceptionEvent)
  652. */
  653. public function setItem($key, $value)
  654. {
  655. $options = $this->getOptions();
  656. if ($options->getWritable() && $options->getClearStatCache()) {
  657. clearstatcache();
  658. }
  659. return parent::setItem($key, $value);
  660. }
  661. /**
  662. * Store multiple items.
  663. *
  664. * @param array $keyValuePairs
  665. * @return array Array of not stored keys
  666. * @throws Exception\ExceptionInterface
  667. *
  668. * @triggers setItems.pre(PreEvent)
  669. * @triggers setItems.post(PostEvent)
  670. * @triggers setItems.exception(ExceptionEvent)
  671. */
  672. public function setItems(array $keyValuePairs)
  673. {
  674. $options = $this->getOptions();
  675. if ($options->getWritable() && $options->getClearStatCache()) {
  676. clearstatcache();
  677. }
  678. return parent::setItems($keyValuePairs);
  679. }
  680. /**
  681. * Add an item.
  682. *
  683. * @param string $key
  684. * @param mixed $value
  685. * @return boolean
  686. * @throws Exception\ExceptionInterface
  687. *
  688. * @triggers addItem.pre(PreEvent)
  689. * @triggers addItem.post(PostEvent)
  690. * @triggers addItem.exception(ExceptionEvent)
  691. */
  692. public function addItem($key, $value)
  693. {
  694. $options = $this->getOptions();
  695. if ($options->getWritable() && $options->getClearStatCache()) {
  696. clearstatcache();
  697. }
  698. return parent::addItem($key, $value);
  699. }
  700. /**
  701. * Add multiple items.
  702. *
  703. * @param array $keyValuePairs
  704. * @return boolean
  705. * @throws Exception\ExceptionInterface
  706. *
  707. * @triggers addItems.pre(PreEvent)
  708. * @triggers addItems.post(PostEvent)
  709. * @triggers addItems.exception(ExceptionEvent)
  710. */
  711. public function addItems(array $keyValuePairs)
  712. {
  713. $options = $this->getOptions();
  714. if ($options->getWritable() && $options->getClearStatCache()) {
  715. clearstatcache();
  716. }
  717. return parent::addItems($keyValuePairs);
  718. }
  719. /**
  720. * Replace an existing item.
  721. *
  722. * @param string $key
  723. * @param mixed $value
  724. * @return boolean
  725. * @throws Exception\ExceptionInterface
  726. *
  727. * @triggers replaceItem.pre(PreEvent)
  728. * @triggers replaceItem.post(PostEvent)
  729. * @triggers replaceItem.exception(ExceptionEvent)
  730. */
  731. public function replaceItem($key, $value)
  732. {
  733. $options = $this->getOptions();
  734. if ($options->getWritable() && $options->getClearStatCache()) {
  735. clearstatcache();
  736. }
  737. return parent::replaceItem($key, $value);
  738. }
  739. /**
  740. * Replace multiple existing items.
  741. *
  742. * @param array $keyValuePairs
  743. * @return boolean
  744. * @throws Exception\ExceptionInterface
  745. *
  746. * @triggers replaceItems.pre(PreEvent)
  747. * @triggers replaceItems.post(PostEvent)
  748. * @triggers replaceItems.exception(ExceptionEvent)
  749. */
  750. public function replaceItems(array $keyValuePairs)
  751. {
  752. $options = $this->getOptions();
  753. if ($options->getWritable() && $options->getClearStatCache()) {
  754. clearstatcache();
  755. }
  756. return parent::replaceItems($keyValuePairs);
  757. }
  758. /**
  759. * Internal method to store an item.
  760. *
  761. * @param string $normalizedKey
  762. * @param mixed $value
  763. * @return boolean
  764. * @throws Exception\ExceptionInterface
  765. */
  766. protected function internalSetItem(& $normalizedKey, & $value)
  767. {
  768. $filespec = $this->getFileSpec($normalizedKey);
  769. $this->prepareDirectoryStructure($filespec);
  770. $this->putFileContent($filespec . '.dat', $value);
  771. $this->unlink($filespec . '.tag');
  772. return true;
  773. }
  774. /**
  775. * Internal method to store multiple items.
  776. *
  777. * @param array $normalizedKeyValuePairs
  778. * @return array Array of not stored keys
  779. * @throws Exception\ExceptionInterface
  780. */
  781. protected function internalSetItems(array & $normalizedKeyValuePairs)
  782. {
  783. $oldUmask = null;
  784. // create an associated array of files and contents to write
  785. $contents = array();
  786. foreach ($normalizedKeyValuePairs as $key => & $value) {
  787. $filespec = $this->getFileSpec($key);
  788. $this->prepareDirectoryStructure($filespec);
  789. // *.dat file
  790. $contents[$filespec . '.dat'] = & $value;
  791. // *.tag file
  792. $this->unlink($filespec . '.tag');
  793. }
  794. // write to disk
  795. while ($contents) {
  796. $nonBlocking = count($contents) > 1;
  797. $wouldblock = null;
  798. foreach ($contents as $file => & $content) {
  799. $this->putFileContent($file, $content, $nonBlocking, $wouldblock);
  800. if (!$nonBlocking || !$wouldblock) {
  801. unset($contents[$file]);
  802. }
  803. }
  804. }
  805. // return OK
  806. return array();
  807. }
  808. /**
  809. * Set an item only if token matches
  810. *
  811. * It uses the token received from getItem() to check if the item has
  812. * changed before overwriting it.
  813. *
  814. * @param mixed $token
  815. * @param string $key
  816. * @param mixed $value
  817. * @return boolean
  818. * @throws Exception\ExceptionInterface
  819. * @see getItem()
  820. * @see setItem()
  821. */
  822. public function checkAndSetItem($token, $key, $value)
  823. {
  824. $options = $this->getOptions();
  825. if ($options->getWritable() && $options->getClearStatCache()) {
  826. clearstatcache();
  827. }
  828. return parent::checkAndSetItem($token, $key, $value);
  829. }
  830. /**
  831. * Internal method to set an item only if token matches
  832. *
  833. * @param mixed $token
  834. * @param string $normalizedKey
  835. * @param mixed $value
  836. * @return boolean
  837. * @throws Exception\ExceptionInterface
  838. * @see getItem()
  839. * @see setItem()
  840. */
  841. protected function internalCheckAndSetItem(& $token, & $normalizedKey, & $value)
  842. {
  843. if (!$this->internalHasItem($normalizedKey)) {
  844. return false;
  845. }
  846. // use filemtime + filesize as CAS token
  847. $file = $this->getFileSpec($normalizedKey) . '.dat';
  848. $check = filemtime($file) . filesize($file);
  849. if ($token !== $check) {
  850. return false;
  851. }
  852. return $this->internalSetItem($normalizedKey, $value);
  853. }
  854. /**
  855. * Reset lifetime of an item
  856. *
  857. * @param string $key
  858. * @return boolean
  859. * @throws Exception\ExceptionInterface
  860. *
  861. * @triggers touchItem.pre(PreEvent)
  862. * @triggers touchItem.post(PostEvent)
  863. * @triggers touchItem.exception(ExceptionEvent)
  864. */
  865. public function touchItem($key)
  866. {
  867. $options = $this->getOptions();
  868. if ($options->getWritable() && $options->getClearStatCache()) {
  869. clearstatcache();
  870. }
  871. return parent::touchItem($key);
  872. }
  873. /**
  874. * Reset lifetime of multiple items.
  875. *
  876. * @param array $keys
  877. * @return array Array of not updated keys
  878. * @throws Exception\ExceptionInterface
  879. *
  880. * @triggers touchItems.pre(PreEvent)
  881. * @triggers touchItems.post(PostEvent)
  882. * @triggers touchItems.exception(ExceptionEvent)
  883. */
  884. public function touchItems(array $keys)
  885. {
  886. $options = $this->getOptions();
  887. if ($options->getWritable() && $options->getClearStatCache()) {
  888. clearstatcache();
  889. }
  890. return parent::touchItems($keys);
  891. }
  892. /**
  893. * Internal method to reset lifetime of an item
  894. *
  895. * @param string $normalizedKey
  896. * @return boolean
  897. * @throws Exception\ExceptionInterface
  898. */
  899. protected function internalTouchItem(& $normalizedKey)
  900. {
  901. if (!$this->internalHasItem($normalizedKey)) {
  902. return false;
  903. }
  904. $filespec = $this->getFileSpec($normalizedKey);
  905. ErrorHandler::start();
  906. $touch = touch($filespec . '.dat');
  907. $error = ErrorHandler::stop();
  908. if (!$touch) {
  909. throw new Exception\RuntimeException(
  910. "Error touching file '{$filespec}.dat'", 0, $error
  911. );
  912. }
  913. return true;
  914. }
  915. /**
  916. * Remove an item.
  917. *
  918. * @param string $key
  919. * @return boolean
  920. * @throws Exception\ExceptionInterface
  921. *
  922. * @triggers removeItem.pre(PreEvent)
  923. * @triggers removeItem.post(PostEvent)
  924. * @triggers removeItem.exception(ExceptionEvent)
  925. */
  926. public function removeItem($key)
  927. {
  928. $options = $this->getOptions();
  929. if ($options->getWritable() && $options->getClearStatCache()) {
  930. clearstatcache();
  931. }
  932. return parent::removeItem($key);
  933. }
  934. /**
  935. * Remove multiple items.
  936. *
  937. * @param array $keys
  938. * @return array Array of not removed keys
  939. * @throws Exception\ExceptionInterface
  940. *
  941. * @triggers removeItems.pre(PreEvent)
  942. * @triggers removeItems.post(PostEvent)
  943. * @triggers removeItems.exception(ExceptionEvent)
  944. */
  945. public function removeItems(array $keys)
  946. {
  947. $options = $this->getOptions();
  948. if ($options->getWritable() && $options->getClearStatCache()) {
  949. clearstatcache();
  950. }
  951. return parent::removeItems($keys);
  952. }
  953. /**
  954. * Internal method to remove an item.
  955. *
  956. * @param string $normalizedKey
  957. * @return boolean
  958. * @throws Exception\ExceptionInterface
  959. */
  960. protected function internalRemoveItem(& $normalizedKey)
  961. {
  962. $filespec = $this->getFileSpec($normalizedKey);
  963. if (!file_exists($filespec . '.dat')) {
  964. return false;
  965. } else {
  966. $this->unlink($filespec . '.dat');
  967. $this->unlink($filespec . '.tag');
  968. }
  969. return true;
  970. }
  971. /* status */
  972. /**
  973. * Internal method to get capabilities of this adapter
  974. *
  975. * @return Capabilities
  976. */
  977. protected function internalGetCapabilities()
  978. {
  979. if ($this->capabilities === null) {
  980. $marker = new stdClass();
  981. $options = $this->getOptions();
  982. // detect metadata
  983. $metadata = array('mtime', 'filespec');
  984. if (!$options->getNoAtime()) {
  985. $metadata[] = 'atime';
  986. }
  987. if (!$options->getNoCtime()) {
  988. $metadata[] = 'ctime';
  989. }
  990. $capabilities = new Capabilities(
  991. $this,
  992. $marker,
  993. array(
  994. 'supportedDatatypes' => array(
  995. 'NULL' => 'string',
  996. 'boolean' => 'string',
  997. 'integer' => 'string',
  998. 'double' => 'string',
  999. 'string' => true,
  1000. 'array' => false,
  1001. 'object' => false,
  1002. 'resource' => false,
  1003. ),
  1004. 'supportedMetadata' => $metadata,
  1005. 'minTtl' => 1,
  1006. 'maxTtl' => 0,
  1007. 'staticTtl' => false,
  1008. 'ttlPrecision' => 1,
  1009. 'expiredRead' => true,
  1010. 'maxKeyLength' => 251, // 255 - strlen(.dat | .tag)
  1011. 'namespaceIsPrefix' => true,
  1012. 'namespaceSeparator' => $options->getNamespaceSeparator(),
  1013. )
  1014. );
  1015. // update capabilities on change options
  1016. $this->getEventManager()->attach('option', function ($event) use ($capabilities, $marker) {
  1017. $params = $event->getParams();
  1018. if (isset($params['namespace_separator'])) {
  1019. $capabilities->setNamespaceSeparator($marker, $params['namespace_separator']);
  1020. }
  1021. if (isset($params['no_atime']) || isset($params['no_ctime'])) {
  1022. $metadata = $capabilities->getSupportedMetadata();
  1023. if (isset($params['no_atime']) && !$params['no_atime']) {
  1024. $metadata[] = 'atime';
  1025. } elseif (isset($params['no_atime']) && ($index = array_search('atime', $metadata)) !== false) {
  1026. unset($metadata[$index]);
  1027. }
  1028. if (isset($params['no_ctime']) && !$params['no_ctime']) {
  1029. $metadata[] = 'ctime';
  1030. } elseif (isset($params['no_ctime']) && ($index = array_search('ctime', $metadata)) !== false) {
  1031. unset($metadata[$index]);
  1032. }
  1033. $capabilities->setSupportedMetadata($marker, $metadata);
  1034. }
  1035. });
  1036. $this->capabilityMarker = $marker;
  1037. $this->capabilities = $capabilities;
  1038. }
  1039. return $this->capabilities;
  1040. }
  1041. /* internal */
  1042. /**
  1043. * Removes directories recursive by namespace
  1044. *
  1045. * @param string $dir Directory to delete
  1046. * @param string $prefix Namespace + Separator
  1047. * @return bool
  1048. */
  1049. protected function rmDir($dir, $prefix)
  1050. {
  1051. $glob = glob(
  1052. $dir . \DIRECTORY_SEPARATOR . $prefix . '*',
  1053. \GLOB_ONLYDIR | \GLOB_NOESCAPE | \GLOB_NOSORT
  1054. );
  1055. if (!$glob) {
  1056. // On some systems glob returns false even on empty result
  1057. return true;
  1058. }
  1059. $ret = true;
  1060. foreach ($glob as $subdir) {
  1061. // skip removing current directory if removing of sub-directory failed
  1062. if ($this->rmDir($subdir, $prefix)) {
  1063. // ignore not empty directories
  1064. ErrorHandler::start();
  1065. $ret = rmdir($subdir) && $ret;
  1066. ErrorHandler::stop();
  1067. } else {
  1068. $ret = false;
  1069. }
  1070. }
  1071. return $ret;
  1072. }
  1073. /**
  1074. * Get file spec of the given key and namespace
  1075. *
  1076. * @param string $normalizedKey
  1077. * @return string
  1078. */
  1079. protected function getFileSpec($normalizedKey)
  1080. {
  1081. $options = $this->getOptions();
  1082. $prefix = $options->getNamespace() . $options->getNamespaceSeparator();
  1083. $path = $options->getCacheDir() . \DIRECTORY_SEPARATOR;
  1084. $level = $options->getDirLevel();
  1085. $fileSpecId = $path . $prefix . $normalizedKey . '/' . $level;
  1086. if ($this->lastFileSpecId !== $fileSpecId) {
  1087. if ($level > 0) {
  1088. // create up to 256 directories per directory level
  1089. $hash = md5($normalizedKey);
  1090. for ($i = 0, $max = ($level * 2); $i < $max; $i+= 2) {
  1091. $path .= $prefix . $hash[$i] . $hash[$i+1] . \DIRECTORY_SEPARATOR;
  1092. }
  1093. }
  1094. $this->lastFileSpecId = $fileSpecId;
  1095. $this->lastFileSpec = $path . $prefix . $normalizedKey;
  1096. }
  1097. return $this->lastFileSpec;
  1098. }
  1099. /**
  1100. * Read info file
  1101. *
  1102. * @param string $file
  1103. * @param boolean $nonBlocking Don't block script if file is locked
  1104. * @param boolean $wouldblock The optional argument is set to TRUE if the lock would block
  1105. * @return array|boolean The info array or false if file wasn't found
  1106. * @throws Exception\RuntimeException
  1107. */
  1108. protected function readInfoFile($file, $nonBlocking = false, & $wouldblock = null)
  1109. {
  1110. if (!file_exists($file)) {
  1111. return false;
  1112. }
  1113. $content = $this->getFileContent($file, $nonBlocking, $wouldblock);
  1114. if ($nonBlocking && $wouldblock) {
  1115. return false;
  1116. }
  1117. ErrorHandler::start();
  1118. $ifo = unserialize($content);
  1119. $err = ErrorHandler::stop();
  1120. if (!is_array($ifo)) {
  1121. throw new Exception\RuntimeException(
  1122. "Corrupted info file '{$file}'", 0, $err
  1123. );
  1124. }
  1125. return $ifo;
  1126. }
  1127. /**
  1128. * Read a complete file
  1129. *
  1130. * @param string $file File complete path
  1131. * @param boolean $nonBlocking Don't block script if file is locked
  1132. * @param boolean $wouldblock The optional argument is set to TRUE if the lock would block
  1133. * @return string
  1134. * @throws Exception\RuntimeException
  1135. */
  1136. protected function getFileContent($file, $nonBlocking = false, & $wouldblock = null)
  1137. {
  1138. $locking = $this->getOptions()->getFileLocking();
  1139. $wouldblock = null;
  1140. ErrorHandler::start();
  1141. // if file locking enabled -> file_get_contents can't be used
  1142. if ($locking) {
  1143. $fp = fopen($file, 'rb');
  1144. if ($fp === false) {
  1145. $err = ErrorHandler::stop();
  1146. throw new Exception\RuntimeException(
  1147. "Error opening file '{$file}'", 0, $err
  1148. );
  1149. }
  1150. if ($nonBlocking) {
  1151. $lock = flock($fp, \LOCK_SH | \LOCK_NB, $wouldblock);
  1152. if ($wouldblock) {
  1153. fclose($fp);
  1154. ErrorHandler::stop();
  1155. return;
  1156. }
  1157. } else {
  1158. $lock = flock($fp, \LOCK_SH);
  1159. }
  1160. if (!$lock) {
  1161. fclose($fp);
  1162. $err = ErrorHandler::stop();
  1163. throw new Exception\RuntimeException(
  1164. "Error locking file '{$file}'", 0, $err
  1165. );
  1166. }
  1167. $res = stream_get_contents($fp);
  1168. if ($res === false) {
  1169. flock($fp, \LOCK_UN);
  1170. fclose($fp);
  1171. $err = ErrorHandler::stop();
  1172. throw new Exception\RuntimeException(
  1173. 'Error getting stream contents', 0, $err
  1174. );
  1175. }
  1176. flock($fp, \LOCK_UN);
  1177. fclose($fp);
  1178. // if file locking disabled -> file_get_contents can be used
  1179. } else {
  1180. $res = file_get_contents($file, false);
  1181. if ($res === false) {
  1182. $err = ErrorHandler::stop();
  1183. throw new Exception\RuntimeException(
  1184. "Error getting file contents for file '{$file}'", 0, $err
  1185. );
  1186. }
  1187. }
  1188. ErrorHandler::stop();
  1189. return $res;
  1190. }
  1191. /**
  1192. * Prepares a directory structure for the given file(spec)
  1193. * using the configured directory level.
  1194. *
  1195. * @param string $file
  1196. * @return void
  1197. * @throws Exception\RuntimeException
  1198. */
  1199. protected function prepareDirectoryStructure($file)
  1200. {
  1201. $options = $this->getOptions();
  1202. $level = $options->getDirLevel();
  1203. // Directory structure is required only if directory level > 0
  1204. if (!$level) {
  1205. return;
  1206. }
  1207. // Directory structure already exists
  1208. $pathname = dirname($file);
  1209. if (file_exists($pathname)) {
  1210. return;
  1211. }
  1212. $perm = $options->getDirPermission();
  1213. $umask = $options->getUmask();
  1214. if ($umask !== false && $perm !== false) {
  1215. $perm = $perm & ~$umask;
  1216. }
  1217. ErrorHandler::start();
  1218. if ($perm === false || $level == 1) {
  1219. // build-in mkdir function is enough
  1220. $umask = ($umask !== false) ? umask($umask) : false;
  1221. $res = mkdir($pathname, ($perm !== false) ? $perm : 0777, true);
  1222. if ($umask !== false) {
  1223. umask($umask);
  1224. }
  1225. if (!$res) {
  1226. $oct = ($perm === false) ? '777' : decoct($perm);
  1227. $err = ErrorHandler::stop();
  1228. throw new Exception\RuntimeException(
  1229. "mkdir('{$pathname}', 0{$oct}, true) failed", 0, $err
  1230. );
  1231. }
  1232. if ($perm !== false && !chmod($pathname, $perm)) {
  1233. $oct = decoct($perm);
  1234. $err = ErrorHandler::stop();
  1235. throw new Exception\RuntimeException(
  1236. "chmod('{$pathname}', 0{$oct}) failed", 0, $err
  1237. );
  1238. }
  1239. } else {
  1240. // build-in mkdir function sets permission together with current umask
  1241. // which doesn't work well on multo threaded webservers
  1242. // -> create directories one by one and set permissions
  1243. // find existing path and missing path parts
  1244. $parts = array();
  1245. $path = $pathname;
  1246. while (!file_exists($path)) {
  1247. array_unshift($parts, basename($path));
  1248. $nextPath = dirname($path);
  1249. if ($nextPath === $path) {
  1250. break;
  1251. }
  1252. $path = $nextPath;
  1253. }
  1254. // make all missing path parts
  1255. foreach ($parts as $part) {
  1256. $path.= \DIRECTORY_SEPARATOR . $part;
  1257. // create a single directory, set and reset umask immediately
  1258. $umask = ($umask !== false) ? umask($umask) : false;
  1259. $res = mkdir($path, ($perm === false) ? 0777 : $perm, false);
  1260. if ($umask !== false) {
  1261. umask($umask);
  1262. }
  1263. if (!$res) {
  1264. $oct = ($perm === false) ? '777' : decoct($perm);
  1265. $err = ErrorHandler::stop();
  1266. throw new Exception\RuntimeException(
  1267. "mkdir('{$path}', 0{$oct}, false) failed"
  1268. );
  1269. }
  1270. if ($perm !== false && !chmod($path, $perm)) {
  1271. $oct = decoct($perm);
  1272. $err = ErrorHandler::stop();
  1273. throw new Exception\RuntimeException(
  1274. "chmod('{$path}', 0{$oct}) failed"
  1275. );
  1276. }
  1277. }
  1278. }
  1279. ErrorHandler::stop();
  1280. }
  1281. /**
  1282. * Write content to a file
  1283. *
  1284. * @param string $file File complete path
  1285. * @param string $data Data to write
  1286. * @param boolean $nonBlocking Don't block script if file is locked
  1287. * @param boolean $wouldblock The optional argument is set to TRUE if the lock would block
  1288. * @return void
  1289. * @throws Exception\RuntimeException
  1290. */
  1291. protected function putFileContent($file, $data, $nonBlocking = false, & $wouldblock = null)
  1292. {
  1293. $options = $this->getOptions();
  1294. $locking = $options->getFileLocking();
  1295. $nonBlocking = $locking && $nonBlocking;
  1296. $wouldblock = null;
  1297. $umask = $options->getUmask();
  1298. $perm = $options->getFilePermission();
  1299. if ($umask !== false && $perm !== false) {
  1300. $perm = $perm & ~$umask;
  1301. }
  1302. ErrorHandler::start();
  1303. // if locking and non blocking is enabled -> file_put_contents can't used
  1304. if ($locking && $nonBlocking) {
  1305. $umask = ($umask !== false) ? umask($umask) : false;
  1306. $fp = fopen($file, 'cb');
  1307. if ($umask) {
  1308. umask($umask);
  1309. }
  1310. if (!$fp) {
  1311. $err = ErrorHandler::stop();
  1312. throw new Exception\RuntimeException(
  1313. "Error opening file '{$file}'", 0, $err
  1314. );
  1315. }
  1316. if ($perm !== false && !chmod($file, $perm)) {
  1317. fclose($fp);
  1318. $oct = decoct($perm);
  1319. $err = ErrorHandler::stop();
  1320. throw new Exception\RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err);
  1321. }
  1322. if (!flock($fp, \LOCK_EX | \LOCK_NB, $wouldblock)) {
  1323. fclose($fp);
  1324. $err = ErrorHandler::stop();
  1325. if ($wouldblock) {
  1326. return;
  1327. } else {
  1328. throw new Exception\RuntimeException("Error locking file '{$file}'", 0, $err);
  1329. }
  1330. }
  1331. if (!fwrite($fp, $data)) {
  1332. flock($fp, \LOCK_UN);
  1333. fclose($fp);
  1334. $err = ErrorHandler::stop();
  1335. throw new Exception\RuntimeException("Error writing file '{$file}'", 0, $err);
  1336. }
  1337. if (!ftruncate($fp, strlen($data))) {
  1338. flock($fp, \LOCK_UN);
  1339. fclose($fp);
  1340. $err = ErrorHandler::stop();
  1341. throw new Exception\RuntimeException("Error truncating file '{$file}'", 0, $err);
  1342. }
  1343. flock($fp, \LOCK_UN);
  1344. fclose($fp);
  1345. // else -> file_put_contents can be used
  1346. } else {
  1347. $flags = 0;
  1348. if ($locking) {
  1349. $flags = $flags | \LOCK_EX;
  1350. }
  1351. $umask = ($umask !== false) ? umask($umask) : false;
  1352. $rs = file_put_contents($file, $data, $flags);
  1353. if ($umask) {
  1354. umask($umask);
  1355. }
  1356. if ($rs === false) {
  1357. $err = ErrorHandler::stop();
  1358. throw new Exception\RuntimeException(
  1359. "Error writing file '{$file}'", 0, $err
  1360. );
  1361. }
  1362. if ($perm !== false && !chmod($file, $perm)) {
  1363. $oct = decoct($perm);
  1364. $err = ErrorHandler::stop();
  1365. throw new Exception\RuntimeException("chmod('{$file}', 0{$oct}) failed", 0, $err);
  1366. }
  1367. }
  1368. ErrorHandler::stop();
  1369. }
  1370. /**
  1371. * Unlink a file
  1372. *
  1373. * @param string $file
  1374. * @return void
  1375. * @throw RuntimeException
  1376. */
  1377. protected function unlink($file)
  1378. {
  1379. ErrorHandler::start();
  1380. $res = unlink($file);
  1381. $err = ErrorHandler::stop();
  1382. // only throw exception if file still exists after deleting
  1383. if (!$res && file_exists($file)) {
  1384. throw new Exception\RuntimeException(
  1385. "Error unlinking file '{$file}'; file still exists", 0, $err
  1386. );
  1387. }
  1388. }
  1389. }