PageRenderTime 42ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Composer/Cache.php

http://github.com/composer/composer
PHP | 257 lines | 232 code | 5 blank | 20 comment | 0 complexity | aafe861ecfd0a831021492e622afed25 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer;
  12. use Composer\IO\IOInterface;
  13. use Composer\Util\Filesystem;
  14. use Composer\Util\Silencer;
  15. use Symfony\Component\Finder\Finder;
  16. /**
  17. * Reads/writes to a filesystem cache
  18. *
  19. * @author Jordi Boggiano <j.boggiano@seld.be>
  20. */
  21. class Cache
  22. {
  23. private static $cacheCollected = false;
  24. private $io;
  25. private $root;
  26. private $enabled = true;
  27. private $allowlist;
  28. private $filesystem;
  29. /**
  30. * @param IOInterface $io
  31. * @param string $cacheDir location of the cache
  32. * @param string $allowlist List of characters that are allowed in path names (used in a regex character class)
  33. * @param Filesystem $filesystem optional filesystem instance
  34. */
  35. public function __construct(IOInterface $io, $cacheDir, $allowlist = 'a-z0-9.', Filesystem $filesystem = null)
  36. {
  37. $this->io = $io;
  38. $this->root = rtrim($cacheDir, '/\\') . '/';
  39. $this->allowlist = $allowlist;
  40. $this->filesystem = $filesystem ?: new Filesystem();
  41. if (!self::isUsable($cacheDir)) {
  42. $this->enabled = false;
  43. return;
  44. }
  45. if (
  46. (!is_dir($this->root) && !Silencer::call('mkdir', $this->root, 0777, true))
  47. || !is_writable($this->root)
  48. ) {
  49. $this->io->writeError('<warning>Cannot create cache directory ' . $this->root . ', or directory is not writable. Proceeding without cache</warning>');
  50. $this->enabled = false;
  51. }
  52. }
  53. public static function isUsable($path)
  54. {
  55. return !preg_match('{(^|[\\\\/])(\$null|nul|NUL|/dev/null)([\\\\/]|$)}', $path);
  56. }
  57. public function isEnabled()
  58. {
  59. return $this->enabled;
  60. }
  61. public function getRoot()
  62. {
  63. return $this->root;
  64. }
  65. public function read($file)
  66. {
  67. if ($this->enabled) {
  68. $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
  69. if (file_exists($this->root . $file)) {
  70. $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG);
  71. return file_get_contents($this->root . $file);
  72. }
  73. }
  74. return false;
  75. }
  76. public function write($file, $contents)
  77. {
  78. if ($this->enabled) {
  79. $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
  80. $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG);
  81. try {
  82. return file_put_contents($this->root . $file, $contents);
  83. } catch (\ErrorException $e) {
  84. $this->io->writeError('<warning>Failed to write into cache: '.$e->getMessage().'</warning>', true, IOInterface::DEBUG);
  85. if (preg_match('{^file_put_contents\(\): Only ([0-9]+) of ([0-9]+) bytes written}', $e->getMessage(), $m)) {
  86. // Remove partial file.
  87. unlink($this->root . $file);
  88. $message = sprintf(
  89. '<warning>Writing %1$s into cache failed after %2$u of %3$u bytes written, only %4$u bytes of free space available</warning>',
  90. $this->root . $file,
  91. $m[1],
  92. $m[2],
  93. @disk_free_space($this->root . dirname($file))
  94. );
  95. $this->io->writeError($message);
  96. return false;
  97. }
  98. throw $e;
  99. }
  100. }
  101. return false;
  102. }
  103. /**
  104. * Copy a file into the cache
  105. */
  106. public function copyFrom($file, $source)
  107. {
  108. if ($this->enabled) {
  109. $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
  110. $this->filesystem->ensureDirectoryExists(dirname($this->root . $file));
  111. if (!file_exists($source)) {
  112. $this->io->writeError('<error>'.$source.' does not exist, can not write into cache</error>');
  113. } elseif ($this->io->isDebug()) {
  114. $this->io->writeError('Writing '.$this->root . $file.' into cache from '.$source);
  115. }
  116. return copy($source, $this->root . $file);
  117. }
  118. return false;
  119. }
  120. /**
  121. * Copy a file out of the cache
  122. */
  123. public function copyTo($file, $target)
  124. {
  125. if ($this->enabled) {
  126. $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
  127. if (file_exists($this->root . $file)) {
  128. try {
  129. touch($this->root . $file, filemtime($this->root . $file), time());
  130. } catch (\ErrorException $e) {
  131. // fallback in case the above failed due to incorrect ownership
  132. // see https://github.com/composer/composer/issues/4070
  133. Silencer::call('touch', $this->root . $file);
  134. }
  135. $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG);
  136. return copy($this->root . $file, $target);
  137. }
  138. }
  139. return false;
  140. }
  141. public function gcIsNecessary()
  142. {
  143. return (!self::$cacheCollected && !mt_rand(0, 50));
  144. }
  145. public function remove($file)
  146. {
  147. if ($this->enabled) {
  148. $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
  149. if (file_exists($this->root . $file)) {
  150. return $this->filesystem->unlink($this->root . $file);
  151. }
  152. }
  153. return false;
  154. }
  155. public function clear()
  156. {
  157. if ($this->enabled) {
  158. $this->filesystem->emptyDirectory($this->root);
  159. return true;
  160. }
  161. return false;
  162. }
  163. public function gc($ttl, $maxSize)
  164. {
  165. if ($this->enabled) {
  166. $expire = new \DateTime();
  167. $expire->modify('-'.$ttl.' seconds');
  168. $finder = $this->getFinder()->date('until '.$expire->format('Y-m-d H:i:s'));
  169. foreach ($finder as $file) {
  170. $this->filesystem->unlink($file->getPathname());
  171. }
  172. $totalSize = $this->filesystem->size($this->root);
  173. if ($totalSize > $maxSize) {
  174. $iterator = $this->getFinder()->sortByAccessedTime()->getIterator();
  175. while ($totalSize > $maxSize && $iterator->valid()) {
  176. $filepath = $iterator->current()->getPathname();
  177. $totalSize -= $this->filesystem->size($filepath);
  178. $this->filesystem->unlink($filepath);
  179. $iterator->next();
  180. }
  181. }
  182. self::$cacheCollected = true;
  183. return true;
  184. }
  185. return false;
  186. }
  187. public function sha1($file)
  188. {
  189. if ($this->enabled) {
  190. $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
  191. if (file_exists($this->root . $file)) {
  192. return sha1_file($this->root . $file);
  193. }
  194. }
  195. return false;
  196. }
  197. public function sha256($file)
  198. {
  199. if ($this->enabled) {
  200. $file = preg_replace('{[^'.$this->allowlist.']}i', '-', $file);
  201. if (file_exists($this->root . $file)) {
  202. return hash_file('sha256', $this->root . $file);
  203. }
  204. }
  205. return false;
  206. }
  207. protected function getFinder()
  208. {
  209. return Finder::create()->in($this->root)->files();
  210. }
  211. }