FileDownloader.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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 && !mt_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. if (!$this->outputProgress) {
  85. $this->io->write(' Downloading');
  86. }
  87. // try to download 3 times then fail hard
  88. $retries = 3;
  89. while ($retries--) {
  90. try {
  91. $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
  92. break;
  93. } catch (TransportException $e) {
  94. // if we got an http response with a proper code, then requesting again will probably not help, abort
  95. if ((0 !== $e->getCode() && 500 !== $e->getCode()) || !$retries) {
  96. throw $e;
  97. }
  98. if ($this->io->isVerbose()) {
  99. $this->io->write(' Download failed, retrying...');
  100. }
  101. usleep(500000);
  102. }
  103. }
  104. if ($this->cache) {
  105. $this->cache->copyFrom($this->getCacheKey($package), $fileName);
  106. }
  107. } else {
  108. $this->io->write(' Loading from cache');
  109. }
  110. } catch (TransportException $e) {
  111. if (in_array($e->getCode(), array(404, 403)) && 'github.com' === $hostname && !$this->io->hasAuthentication($hostname)) {
  112. $message = "\n".'Could not fetch '.$processedUrl.', enter your GitHub credentials '.($e->getCode() === 404 ? 'to access private repos' : 'to go over the API rate limit');
  113. $gitHubUtil = new GitHub($this->io, $this->config, null, $this->rfs);
  114. if (!$gitHubUtil->authorizeOAuth($hostname)
  115. && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($hostname, $message))
  116. ) {
  117. throw $e;
  118. }
  119. $this->rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
  120. } else {
  121. throw $e;
  122. }
  123. }
  124. if (!file_exists($fileName)) {
  125. throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
  126. .' directory is writable and you have internet connectivity');
  127. }
  128. $checksum = $package->getDistSha1Checksum();
  129. if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
  130. throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')');
  131. }
  132. } catch (\Exception $e) {
  133. // clean up
  134. $this->filesystem->removeDirectory($path);
  135. $this->clearCache($package, $path);
  136. throw $e;
  137. }
  138. return $fileName;
  139. }
  140. /**
  141. * {@inheritDoc}
  142. */
  143. public function setOutputProgress($outputProgress)
  144. {
  145. $this->outputProgress = $outputProgress;
  146. return $this;
  147. }
  148. protected function clearCache(PackageInterface $package, $path)
  149. {
  150. if ($this->cache) {
  151. $fileName = $this->getFileName($package, $path);
  152. $this->cache->remove($this->getCacheKey($package));
  153. }
  154. }
  155. /**
  156. * {@inheritDoc}
  157. */
  158. public function update(PackageInterface $initial, PackageInterface $target, $path)
  159. {
  160. $this->remove($initial, $path);
  161. $this->download($target, $path);
  162. }
  163. /**
  164. * {@inheritDoc}
  165. */
  166. public function remove(PackageInterface $package, $path)
  167. {
  168. $this->io->write(" - Removing <info>" . $package->getName() . "</info> (<comment>" . VersionParser::formatVersion($package) . "</comment>)");
  169. if (!$this->filesystem->removeDirectory($path)) {
  170. // retry after a bit on windows since it tends to be touchy with mass removals
  171. if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(250000) && !$this->filesystem->removeDirectory($path))) {
  172. throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
  173. }
  174. }
  175. }
  176. /**
  177. * Gets file name for specific package
  178. *
  179. * @param PackageInterface $package package instance
  180. * @param string $path download path
  181. * @return string file name
  182. */
  183. protected function getFileName(PackageInterface $package, $path)
  184. {
  185. return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
  186. }
  187. /**
  188. * Process the download url
  189. *
  190. * @param PackageInterface $package package the url is coming from
  191. * @param string $url download url
  192. * @return string url
  193. *
  194. * @throws \RuntimeException If any problem with the url
  195. */
  196. protected function processUrl(PackageInterface $package, $url)
  197. {
  198. if (!extension_loaded('openssl') && 0 === strpos($url, 'https:')) {
  199. throw new \RuntimeException('You must enable the openssl extension to download files via https');
  200. }
  201. return $url;
  202. }
  203. private function getCacheKey(PackageInterface $package)
  204. {
  205. if (preg_match('{^[a-f0-9]{40}$}', $package->getDistReference())) {
  206. return $package->getName().'/'.$package->getDistReference().'.'.$package->getDistType();
  207. }
  208. return $package->getName().'/'.$package->getVersion().'-'.$package->getDistReference().'.'.$package->getDistType();
  209. }
  210. }