FileHandler.php 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Cache;
  13. use Symfony\Component\Filesystem\Exception\IOException;
  14. /**
  15. * @author Andreas Möller <am@localheinz.com>
  16. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  17. *
  18. * @internal
  19. */
  20. final class FileHandler implements FileHandlerInterface
  21. {
  22. private \SplFileInfo $fileInfo;
  23. private int $fileMTime = 0;
  24. public function __construct(string $file)
  25. {
  26. $this->fileInfo = new \SplFileInfo($file);
  27. }
  28. public function getFile(): string
  29. {
  30. return $this->fileInfo->getPathname();
  31. }
  32. public function read(): ?CacheInterface
  33. {
  34. if (!$this->fileInfo->isFile() || !$this->fileInfo->isReadable()) {
  35. return null;
  36. }
  37. $fileObject = $this->fileInfo->openFile('r');
  38. $cache = $this->readFromHandle($fileObject);
  39. $this->fileMTime = $this->getFileCurrentMTime();
  40. unset($fileObject); // explicitly close file handler
  41. return $cache;
  42. }
  43. public function write(CacheInterface $cache): void
  44. {
  45. $this->ensureFileIsWriteable();
  46. $fileObject = $this->fileInfo->openFile('r+');
  47. if (method_exists($cache, 'backfillHashes') && $this->fileMTime < $this->getFileCurrentMTime()) {
  48. $resultOfFlock = $fileObject->flock(LOCK_EX);
  49. if (false === $resultOfFlock) {
  50. // Lock failed, OK - we continue without the lock.
  51. // noop
  52. }
  53. $oldCache = $this->readFromHandle($fileObject);
  54. $fileObject->rewind();
  55. if (null !== $oldCache) {
  56. $cache->backfillHashes($oldCache);
  57. }
  58. }
  59. $resultOfTruncate = $fileObject->ftruncate(0);
  60. if (false === $resultOfTruncate) {
  61. // Truncate failed. OK - we do not save the cache.
  62. return;
  63. }
  64. $resultOfWrite = $fileObject->fwrite($cache->toJson());
  65. if (false === $resultOfWrite) {
  66. // Write failed. OK - we did not save the cache.
  67. return;
  68. }
  69. $resultOfFlush = $fileObject->fflush();
  70. if (false === $resultOfFlush) {
  71. // Flush failed. OK - part of cache can be missing, in case this was last chunk in this pid.
  72. // noop
  73. }
  74. $this->fileMTime = time(); // we could take the fresh `mtime` of file that we just modified with `$this->getFileCurrentMTime()`, but `time()` should be good enough here and reduce IO operation
  75. }
  76. private function getFileCurrentMTime(): int
  77. {
  78. clearstatcache(true, $this->fileInfo->getPathname());
  79. $mtime = $this->fileInfo->getMTime();
  80. if (false === $mtime) {
  81. // cannot check mtime? OK - let's pretend file is old.
  82. $mtime = 0;
  83. }
  84. return $mtime;
  85. }
  86. private function readFromHandle(\SplFileObject $fileObject): ?CacheInterface
  87. {
  88. try {
  89. $size = $fileObject->getSize();
  90. if (false === $size || 0 === $size) {
  91. return null;
  92. }
  93. $content = $fileObject->fread($size);
  94. if (false === $content) {
  95. return null;
  96. }
  97. return Cache::fromJson($content);
  98. } catch (\InvalidArgumentException $exception) {
  99. return null;
  100. }
  101. }
  102. private function ensureFileIsWriteable(): void
  103. {
  104. if ($this->fileInfo->isFile() && $this->fileInfo->isWritable()) {
  105. // all good
  106. return;
  107. }
  108. if ($this->fileInfo->isDir()) {
  109. throw new IOException(
  110. sprintf('Cannot write cache file "%s" as the location exists as directory.', $this->fileInfo->getRealPath()),
  111. 0,
  112. null,
  113. $this->fileInfo->getPathname()
  114. );
  115. }
  116. if ($this->fileInfo->isFile() && !$this->fileInfo->isWritable()) {
  117. throw new IOException(
  118. sprintf('Cannot write to file "%s" as it is not writable.', $this->fileInfo->getRealPath()),
  119. 0,
  120. null,
  121. $this->fileInfo->getPathname()
  122. );
  123. }
  124. $this->createFile($this->fileInfo->getPathname());
  125. }
  126. private function createFile(string $file): void
  127. {
  128. $dir = \dirname($file);
  129. // Ensure path is created, but ignore if already exists. FYI: ignore EA suggestion in IDE,
  130. // `mkdir()` returns `false` for existing paths, so we can't mix it with `is_dir()` in one condition.
  131. if (!@is_dir($dir)) {
  132. @mkdir($dir, 0777, true);
  133. }
  134. if (!@is_dir($dir)) {
  135. throw new IOException(
  136. sprintf('Directory of cache file "%s" does not exists and couldn\'t be created.', $file),
  137. 0,
  138. null,
  139. $file
  140. );
  141. }
  142. @touch($file);
  143. @chmod($file, 0666);
  144. }
  145. }