PageRenderTime 56ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

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

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