PageRenderTime 61ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

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

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