FileDownloader.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  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\Downloader;
  12. use Composer\Config;
  13. use Composer\Cache;
  14. use Composer\IO\IOInterface;
  15. use Composer\Package\PackageInterface;
  16. use Composer\Package\Version\VersionParser;
  17. use Composer\Util\Filesystem;
  18. use Composer\Util\GitHub;
  19. use Composer\Util\RemoteFilesystem;
  20. /**
  21. * Base downloader for files
  22. *
  23. * @author Kirill chEbba Chebunin <iam@chebba.org>
  24. * @author Jordi Boggiano <j.boggiano@seld.be>
  25. * @author François Pluchino <francois.pluchino@opendisplay.com>
  26. */
  27. class FileDownloader implements DownloaderInterface
  28. {
  29. private static $cacheCollected = false;
  30. protected $io;
  31. protected $config;
  32. protected $rfs;
  33. protected $filesystem;
  34. protected $cache;
  35. protected $outputProgress = true;
  36. /**
  37. * Constructor.
  38. *
  39. * @param IOInterface $io The IO instance
  40. * @param Config $config The config
  41. * @param Cache $cache Optional cache instance
  42. * @param RemoteFilesystem $rfs The remote filesystem
  43. * @param Filesystem $filesystem The filesystem
  44. */
  45. public function __construct(IOInterface $io, Config $config, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
  46. {
  47. $this->io = $io;
  48. $this->config = $config;
  49. $this->rfs = $rfs ?: new RemoteFilesystem($io);
  50. $this->filesystem = $filesystem ?: new Filesystem();
  51. $this->cache = $cache;
  52. if ($this->cache && !self::$cacheCollected && !rand(0, 50)) {
  53. $this->cache->gc($config->get('cache-ttl'), $config->get('cache-files-maxsize'));
  54. }
  55. self::$cacheCollected = true;
  56. }
  57. /**
  58. * {@inheritDoc}
  59. */
  60. public function getInstallationSource()
  61. {
  62. return 'dist';
  63. }
  64. /**
  65. * {@inheritDoc}
  66. */
  67. public function download(PackageInterface $package, $path)
  68. {
  69. $url = $package->getDistUrl();
  70. if (!$url) {
  71. throw new \InvalidArgumentException('The given package is missing url information');
  72. }
  73. $this->filesystem->ensureDirectoryExists($path);
  74. $fileName = $this->getFileName($package, $path);
  75. $this->io->write(" - Installing <info>" . $package->getName() . "</info> (<comment>" . VersionParser::formatVersion($package) . "</comment>)");
  76. $processedUrl = $this->processUrl($package, $url);
  77. $hostname = parse_url($processedUrl, PHP_URL_HOST);
  78. if (strpos($hostname, '.github.com') === (strlen($hostname) - 11)) {
  79. $hostname = 'github.com';
  80. }
  81. try {
  82. try {
  83. if (!$this->cache || !$this->cache->copyTo($this->getCacheKey($package), $fileName)) {
  84. $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
  85. if (!$this->outputProgress) {
  86. $this->io->write(' Downloading');
  87. }
  88. if ($this->cache) {
  89. $this->cache->copyFrom($this->getCacheKey($package), $fileName);
  90. }
  91. } else {
  92. $this->io->write(' Loading from cache');
  93. }
  94. } catch (TransportException $e) {
  95. if (in_array($e->getCode(), array(404, 403)) && 'github.com' === $hostname && !$this->io->hasAuthentication($hostname)) {
  96. $message = "\n".'Could not fetch '.$processedUrl.', enter your GitHub credentials '.($e->getCode === 404 ? 'to access private repos' : 'to go over the API rate limit');
  97. $gitHubUtil = new GitHub($this->io, $this->config, null, $this->rfs);
  98. if (!$gitHubUtil->authorizeOAuth($hostname)
  99. && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($hostname, $message))
  100. ) {
  101. throw $e;
  102. }
  103. $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
  104. } else {
  105. throw $e;
  106. }
  107. }
  108. if (!file_exists($fileName)) {
  109. throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
  110. .' directory is writable and you have internet connectivity');
  111. }
  112. $checksum = $package->getDistSha1Checksum();
  113. if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
  114. throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')');
  115. }
  116. } catch (\Exception $e) {
  117. // clean up
  118. $this->filesystem->removeDirectory($path);
  119. $this->clearCache($package, $path);
  120. throw $e;
  121. }
  122. }
  123. /**
  124. * {@inheritDoc}
  125. */
  126. public function setOutputProgress($outputProgress)
  127. {
  128. $this->outputProgress = $outputProgress;
  129. return $this;
  130. }
  131. protected function clearCache(PackageInterface $package, $path)
  132. {
  133. if ($this->cache) {
  134. $fileName = $this->getFileName($package, $path);
  135. $this->cache->remove($this->getCacheKey($package));
  136. }
  137. }
  138. /**
  139. * {@inheritDoc}
  140. */
  141. public function update(PackageInterface $initial, PackageInterface $target, $path)
  142. {
  143. $this->remove($initial, $path);
  144. $this->download($target, $path);
  145. }
  146. /**
  147. * {@inheritDoc}
  148. */
  149. public function remove(PackageInterface $package, $path)
  150. {
  151. $this->io->write(" - Removing <info>" . $package->getName() . "</info> (<comment>" . VersionParser::formatVersion($package) . "</comment>)");
  152. if (!$this->filesystem->removeDirectory($path)) {
  153. throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
  154. }
  155. }
  156. /**
  157. * Gets file name for specific package
  158. *
  159. * @param PackageInterface $package package instance
  160. * @param string $path download path
  161. * @return string file name
  162. */
  163. protected function getFileName(PackageInterface $package, $path)
  164. {
  165. return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
  166. }
  167. /**
  168. * Process the download url
  169. *
  170. * @param PackageInterface $package package the url is coming from
  171. * @param string $url download url
  172. * @return string url
  173. *
  174. * @throws \RuntimeException If any problem with the url
  175. */
  176. protected function processUrl(PackageInterface $package, $url)
  177. {
  178. if (!extension_loaded('openssl') && 0 === strpos($url, 'https:')) {
  179. throw new \RuntimeException('You must enable the openssl extension to download files via https');
  180. }
  181. return $url;
  182. }
  183. private function getCacheKey(PackageInterface $package)
  184. {
  185. if (preg_match('{^[a-f0-9]{40}$}', $package->getDistReference())) {
  186. return $package->getName().'/'.$package->getDistReference().'.'.$package->getDistType();
  187. }
  188. return $package->getName().'/'.$package->getVersion().'-'.$package->getDistReference().'.'.$package->getDistType();
  189. }
  190. }