FileDownloader.php 9.3 KB

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