FileDownloader.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  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\Plugin\PluginEvents;
  18. use Composer\Plugin\PreFileDownloadEvent;
  19. use Composer\EventDispatcher\EventDispatcher;
  20. use Composer\Util\Filesystem;
  21. use Composer\Util\GitHub;
  22. use Composer\Util\RemoteFilesystem;
  23. /**
  24. * Base downloader for files
  25. *
  26. * @author Kirill chEbba Chebunin <iam@chebba.org>
  27. * @author Jordi Boggiano <j.boggiano@seld.be>
  28. * @author François Pluchino <francois.pluchino@opendisplay.com>
  29. * @author Nils Adermann <naderman@naderman.de>
  30. */
  31. class FileDownloader implements DownloaderInterface
  32. {
  33. private static $cacheCollected = false;
  34. protected $io;
  35. protected $config;
  36. protected $rfs;
  37. protected $filesystem;
  38. protected $cache;
  39. protected $outputProgress = true;
  40. /**
  41. * Constructor.
  42. *
  43. * @param IOInterface $io The IO instance
  44. * @param Config $config The config
  45. * @param EventDispatcher $eventDispatcher The event dispatcher
  46. * @param Cache $cache Optional cache instance
  47. * @param RemoteFilesystem $rfs The remote filesystem
  48. * @param Filesystem $filesystem The filesystem
  49. */
  50. public function __construct(IOInterface $io, Config $config, EventDispatcher $eventDispatcher = null, Cache $cache = null, RemoteFilesystem $rfs = null, Filesystem $filesystem = null)
  51. {
  52. $this->io = $io;
  53. $this->config = $config;
  54. $this->eventDispatcher = $eventDispatcher;
  55. $this->rfs = $rfs ?: new RemoteFilesystem($io);
  56. $this->filesystem = $filesystem ?: new Filesystem();
  57. $this->cache = $cache;
  58. if ($this->cache && !self::$cacheCollected && !mt_rand(0, 50)) {
  59. $this->cache->gc($config->get('cache-ttl'), $config->get('cache-files-maxsize'));
  60. }
  61. self::$cacheCollected = true;
  62. }
  63. /**
  64. * {@inheritDoc}
  65. */
  66. public function getInstallationSource()
  67. {
  68. return 'dist';
  69. }
  70. /**
  71. * {@inheritDoc}
  72. */
  73. public function download(PackageInterface $package, $path)
  74. {
  75. $url = $package->getDistUrl();
  76. if (!$url) {
  77. throw new \InvalidArgumentException('The given package is missing url information');
  78. }
  79. $this->filesystem->ensureDirectoryExists($path);
  80. $fileName = $this->getFileName($package, $path);
  81. $this->io->write(" - Installing <info>" . $package->getName() . "</info> (<comment>" . VersionParser::formatVersion($package) . "</comment>)");
  82. $processedUrl = $this->processUrl($package, $url);
  83. $hostname = parse_url($processedUrl, PHP_URL_HOST);
  84. $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->rfs, $processedUrl);
  85. if ($this->eventDispatcher) {
  86. $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent);
  87. }
  88. $rfs = $preFileDownloadEvent->getRemoteFilesystem();
  89. if (strpos($hostname, '.github.com') === (strlen($hostname) - 11)) {
  90. $hostname = 'github.com';
  91. }
  92. try {
  93. try {
  94. if (!$this->cache || !$this->cache->copyTo($this->getCacheKey($package), $fileName)) {
  95. if (!$this->outputProgress) {
  96. $this->io->write(' Downloading');
  97. }
  98. // try to download 3 times then fail hard
  99. $retries = 3;
  100. while ($retries--) {
  101. try {
  102. $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
  103. break;
  104. } catch (TransportException $e) {
  105. // if we got an http response with a proper code, then requesting again will probably not help, abort
  106. if ((0 !== $e->getCode() && 500 !== $e->getCode()) || !$retries) {
  107. throw $e;
  108. }
  109. if ($this->io->isVerbose()) {
  110. $this->io->write(' Download failed, retrying...');
  111. }
  112. usleep(500000);
  113. }
  114. }
  115. if ($this->cache) {
  116. $this->cache->copyFrom($this->getCacheKey($package), $fileName);
  117. }
  118. } else {
  119. $this->io->write(' Loading from cache');
  120. }
  121. } catch (TransportException $e) {
  122. if (!in_array($e->getCode(), array(404, 403, 412))) {
  123. throw $e;
  124. }
  125. if ('github.com' === $hostname && !$this->io->hasAuthentication($hostname)) {
  126. $message = "\n".'Could not fetch '.$processedUrl.', enter your GitHub credentials '.($e->getCode() === 404 ? 'to access private repos' : 'to go over the API rate limit');
  127. $gitHubUtil = new GitHub($this->io, $this->config, null, $rfs);
  128. if (!$gitHubUtil->authorizeOAuth($hostname)
  129. && (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($hostname, $message))
  130. ) {
  131. throw $e;
  132. }
  133. $rfs->copy($hostname, $processedUrl, $fileName, $this->outputProgress);
  134. } else {
  135. throw $e;
  136. }
  137. }
  138. if (!file_exists($fileName)) {
  139. throw new \UnexpectedValueException($url.' could not be saved to '.$fileName.', make sure the'
  140. .' directory is writable and you have internet connectivity');
  141. }
  142. $checksum = $package->getDistSha1Checksum();
  143. if ($checksum && hash_file('sha1', $fileName) !== $checksum) {
  144. throw new \UnexpectedValueException('The checksum verification of the file failed (downloaded from '.$url.')');
  145. }
  146. } catch (\Exception $e) {
  147. // clean up
  148. $this->filesystem->removeDirectory($path);
  149. $this->clearCache($package, $path);
  150. throw $e;
  151. }
  152. return $fileName;
  153. }
  154. /**
  155. * {@inheritDoc}
  156. */
  157. public function setOutputProgress($outputProgress)
  158. {
  159. $this->outputProgress = $outputProgress;
  160. return $this;
  161. }
  162. protected function clearCache(PackageInterface $package, $path)
  163. {
  164. if ($this->cache) {
  165. $fileName = $this->getFileName($package, $path);
  166. $this->cache->remove($this->getCacheKey($package));
  167. }
  168. }
  169. /**
  170. * {@inheritDoc}
  171. */
  172. public function update(PackageInterface $initial, PackageInterface $target, $path)
  173. {
  174. $this->remove($initial, $path);
  175. $this->download($target, $path);
  176. }
  177. /**
  178. * {@inheritDoc}
  179. */
  180. public function remove(PackageInterface $package, $path)
  181. {
  182. $this->io->write(" - Removing <info>" . $package->getName() . "</info> (<comment>" . VersionParser::formatVersion($package) . "</comment>)");
  183. if (!$this->filesystem->removeDirectory($path)) {
  184. // retry after a bit on windows since it tends to be touchy with mass removals
  185. if (!defined('PHP_WINDOWS_VERSION_BUILD') || (usleep(250000) && !$this->filesystem->removeDirectory($path))) {
  186. throw new \RuntimeException('Could not completely delete '.$path.', aborting.');
  187. }
  188. }
  189. }
  190. /**
  191. * Gets file name for specific package
  192. *
  193. * @param PackageInterface $package package instance
  194. * @param string $path download path
  195. * @return string file name
  196. */
  197. protected function getFileName(PackageInterface $package, $path)
  198. {
  199. return $path.'/'.pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_BASENAME);
  200. }
  201. /**
  202. * Process the download url
  203. *
  204. * @param PackageInterface $package package the url is coming from
  205. * @param string $url download url
  206. * @return string url
  207. *
  208. * @throws \RuntimeException If any problem with the url
  209. */
  210. protected function processUrl(PackageInterface $package, $url)
  211. {
  212. if (!extension_loaded('openssl') && 0 === strpos($url, 'https:')) {
  213. throw new \RuntimeException('You must enable the openssl extension to download files via https');
  214. }
  215. return $url;
  216. }
  217. private function getCacheKey(PackageInterface $package)
  218. {
  219. if (preg_match('{^[a-f0-9]{40}$}', $package->getDistReference())) {
  220. return $package->getName().'/'.$package->getDistReference().'.'.$package->getDistType();
  221. }
  222. return $package->getName().'/'.$package->getVersion().'-'.$package->getDistReference().'.'.$package->getDistType();
  223. }
  224. }