PageRenderTime 98ms CodeModel.GetById 57ms RepoModel.GetById 1ms app.codeStats 0ms

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

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