DownloadManager.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399
  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\Package\PackageInterface;
  13. use Composer\IO\IOInterface;
  14. use Composer\Util\Filesystem;
  15. use React\Promise\PromiseInterface;
  16. /**
  17. * Downloaders manager.
  18. *
  19. * @author Konstantin Kudryashov <ever.zet@gmail.com>
  20. */
  21. class DownloadManager
  22. {
  23. private $io;
  24. private $httpDownloader;
  25. private $preferDist = false;
  26. private $preferSource = false;
  27. private $packagePreferences = array();
  28. private $filesystem;
  29. private $downloaders = array();
  30. /**
  31. * Initializes download manager.
  32. *
  33. * @param IOInterface $io The Input Output Interface
  34. * @param bool $preferSource prefer downloading from source
  35. * @param Filesystem|null $filesystem custom Filesystem object
  36. */
  37. public function __construct(IOInterface $io, $preferSource = false, Filesystem $filesystem = null)
  38. {
  39. $this->io = $io;
  40. $this->preferSource = $preferSource;
  41. $this->filesystem = $filesystem ?: new Filesystem();
  42. }
  43. /**
  44. * Makes downloader prefer source installation over the dist.
  45. *
  46. * @param bool $preferSource prefer downloading from source
  47. * @return DownloadManager
  48. */
  49. public function setPreferSource($preferSource)
  50. {
  51. $this->preferSource = $preferSource;
  52. return $this;
  53. }
  54. /**
  55. * Makes downloader prefer dist installation over the source.
  56. *
  57. * @param bool $preferDist prefer downloading from dist
  58. * @return DownloadManager
  59. */
  60. public function setPreferDist($preferDist)
  61. {
  62. $this->preferDist = $preferDist;
  63. return $this;
  64. }
  65. /**
  66. * Sets fine tuned preference settings for package level source/dist selection.
  67. *
  68. * @param array $preferences array of preferences by package patterns
  69. * @return DownloadManager
  70. */
  71. public function setPreferences(array $preferences)
  72. {
  73. $this->packagePreferences = $preferences;
  74. return $this;
  75. }
  76. /**
  77. * Sets whether to output download progress information for all registered
  78. * downloaders
  79. *
  80. * @param bool $outputProgress
  81. * @return DownloadManager
  82. */
  83. public function setOutputProgress($outputProgress)
  84. {
  85. foreach ($this->downloaders as $downloader) {
  86. $downloader->setOutputProgress($outputProgress);
  87. }
  88. return $this;
  89. }
  90. /**
  91. * Sets installer downloader for a specific installation type.
  92. *
  93. * @param string $type installation type
  94. * @param DownloaderInterface $downloader downloader instance
  95. * @return DownloadManager
  96. */
  97. public function setDownloader($type, DownloaderInterface $downloader)
  98. {
  99. $type = strtolower($type);
  100. $this->downloaders[$type] = $downloader;
  101. return $this;
  102. }
  103. /**
  104. * Returns downloader for a specific installation type.
  105. *
  106. * @param string $type installation type
  107. * @throws \InvalidArgumentException if downloader for provided type is not registered
  108. * @return DownloaderInterface
  109. */
  110. public function getDownloader($type)
  111. {
  112. $type = strtolower($type);
  113. if (!isset($this->downloaders[$type])) {
  114. throw new \InvalidArgumentException(sprintf('Unknown downloader type: %s. Available types: %s.', $type, implode(', ', array_keys($this->downloaders))));
  115. }
  116. return $this->downloaders[$type];
  117. }
  118. /**
  119. * Returns downloader for already installed package.
  120. *
  121. * @param PackageInterface $package package instance
  122. * @throws \InvalidArgumentException if package has no installation source specified
  123. * @throws \LogicException if specific downloader used to load package with
  124. * wrong type
  125. * @return DownloaderInterface|null
  126. */
  127. public function getDownloaderForPackage(PackageInterface $package)
  128. {
  129. $installationSource = $package->getInstallationSource();
  130. if ('metapackage' === $package->getType()) {
  131. return;
  132. }
  133. if ('dist' === $installationSource) {
  134. $downloader = $this->getDownloader($package->getDistType());
  135. } elseif ('source' === $installationSource) {
  136. $downloader = $this->getDownloader($package->getSourceType());
  137. } else {
  138. throw new \InvalidArgumentException(
  139. 'Package '.$package.' does not have an installation source set'
  140. );
  141. }
  142. if ($installationSource !== $downloader->getInstallationSource()) {
  143. throw new \LogicException(sprintf(
  144. 'Downloader "%s" is a %s type downloader and can not be used to download %s for package %s',
  145. get_class($downloader),
  146. $downloader->getInstallationSource(),
  147. $installationSource,
  148. $package
  149. ));
  150. }
  151. return $downloader;
  152. }
  153. public function getDownloaderType(DownloaderInterface $downloader)
  154. {
  155. return array_search($downloader, $this->downloaders);
  156. }
  157. /**
  158. * Downloads package into target dir.
  159. *
  160. * @param PackageInterface $package package instance
  161. * @param string $targetDir target dir
  162. * @param PackageInterface $prevPackage previous package instance in case of updates
  163. *
  164. * @return PromiseInterface
  165. * @throws \InvalidArgumentException if package have no urls to download from
  166. * @throws \RuntimeException
  167. */
  168. public function download(PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
  169. {
  170. $this->filesystem->ensureDirectoryExists(dirname($targetDir));
  171. $sources = $this->getAvailableSources($package, $prevPackage);
  172. $io = $this->io;
  173. $self = $this;
  174. $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download) {
  175. $source = array_shift($sources);
  176. if ($retry) {
  177. $io->writeError(' <warning>Now trying to download from ' . $source . '</warning>');
  178. }
  179. $package->setInstallationSource($source);
  180. $downloader = $self->getDownloaderForPackage($package);
  181. if (!$downloader) {
  182. return \React\Promise\resolve();
  183. }
  184. $handleError = function ($e) use ($sources, $source, $package, $io, $download) {
  185. if ($e instanceof \RuntimeException) {
  186. if (!$sources) {
  187. throw $e;
  188. }
  189. $io->writeError(
  190. ' <warning>Failed to download '.
  191. $package->getPrettyName().
  192. ' from ' . $source . ': '.
  193. $e->getMessage().'</warning>'
  194. );
  195. return $download(true);
  196. }
  197. throw $e;
  198. };
  199. try {
  200. $result = $downloader->download($package, $targetDir);
  201. } catch (\Exception $e) {
  202. return $handleError($e);
  203. }
  204. if (!$result instanceof PromiseInterface) {
  205. return \React\Promise\resolve($result);
  206. }
  207. $res = $result->then(function ($res) {
  208. return $res;
  209. }, $handleError);
  210. return $res;
  211. };
  212. return $download();
  213. }
  214. /**
  215. * Installs package into target dir.
  216. *
  217. * @param PackageInterface $package package instance
  218. * @param string $targetDir target dir
  219. *
  220. * @throws \InvalidArgumentException if package have no urls to download from
  221. * @throws \RuntimeException
  222. */
  223. public function install(PackageInterface $package, $targetDir)
  224. {
  225. $downloader = $this->getDownloaderForPackage($package);
  226. if ($downloader) {
  227. $downloader->install($package, $targetDir);
  228. }
  229. }
  230. /**
  231. * Updates package from initial to target version.
  232. *
  233. * @param PackageInterface $initial initial package version
  234. * @param PackageInterface $target target package version
  235. * @param string $targetDir target dir
  236. *
  237. * @throws \InvalidArgumentException if initial package is not installed
  238. */
  239. public function update(PackageInterface $initial, PackageInterface $target, $targetDir)
  240. {
  241. $downloader = $this->getDownloaderForPackage($target);
  242. $initialDownloader = $this->getDownloaderForPackage($initial);
  243. // no downloaders present means update from metapackage to metapackage, nothing to do
  244. if (!$initialDownloader && !$downloader) {
  245. return;
  246. }
  247. // if we have a downloader present before, but not after, the package became a metapackage and its files should be removed
  248. if (!$downloader) {
  249. $initialDownloader->remove($initial, $targetDir);
  250. return;
  251. }
  252. $initialType = $this->getDownloaderType($initialDownloader);
  253. $targetType = $this->getDownloaderType($downloader);
  254. if ($initialType === $targetType) {
  255. try {
  256. $downloader->update($initial, $target, $targetDir);
  257. return;
  258. } catch (\RuntimeException $e) {
  259. if (!$this->io->isInteractive()) {
  260. throw $e;
  261. }
  262. $this->io->writeError('<error> Update failed ('.$e->getMessage().')</error>');
  263. if (!$this->io->askConfirmation(' Would you like to try reinstalling the package instead [<comment>yes</comment>]? ', true)) {
  264. throw $e;
  265. }
  266. }
  267. }
  268. // if downloader type changed, or update failed and user asks for reinstall,
  269. // we wipe the dir and do a new install instead of updating it
  270. if ($initialDownloader) {
  271. $initialDownloader->remove($initial, $targetDir);
  272. }
  273. $this->install($target, $targetDir);
  274. }
  275. /**
  276. * Removes package from target dir.
  277. *
  278. * @param PackageInterface $package package instance
  279. * @param string $targetDir target dir
  280. */
  281. public function remove(PackageInterface $package, $targetDir)
  282. {
  283. $downloader = $this->getDownloaderForPackage($package);
  284. if ($downloader) {
  285. $downloader->remove($package, $targetDir);
  286. }
  287. }
  288. /**
  289. * Determines the install preference of a package
  290. *
  291. * @param PackageInterface $package package instance
  292. *
  293. * @return string
  294. */
  295. protected function resolvePackageInstallPreference(PackageInterface $package)
  296. {
  297. foreach ($this->packagePreferences as $pattern => $preference) {
  298. $pattern = '{^'.str_replace('\\*', '.*', preg_quote($pattern)).'$}i';
  299. if (preg_match($pattern, $package->getName())) {
  300. if ('dist' === $preference || (!$package->isDev() && 'auto' === $preference)) {
  301. return 'dist';
  302. }
  303. return 'source';
  304. }
  305. }
  306. return $package->isDev() ? 'source' : 'dist';
  307. }
  308. /**
  309. * @return string[]
  310. */
  311. private function getAvailableSources(PackageInterface $package, PackageInterface $prevPackage = null)
  312. {
  313. $sourceType = $package->getSourceType();
  314. $distType = $package->getDistType();
  315. // add source before dist by default
  316. $sources = array();
  317. if ($sourceType) {
  318. $sources[] = 'source';
  319. }
  320. if ($distType) {
  321. $sources[] = 'dist';
  322. }
  323. if (empty($sources)) {
  324. throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
  325. }
  326. if (
  327. $prevPackage
  328. // if we are updating, we want to keep the same source as the previously installed package (if available in the new one)
  329. && in_array($prevPackage->getInstallationSource(), $sources, true)
  330. // unless the previous package was stable dist (by default) and the new package is dev, then we allow the new default to take over
  331. && !(!$prevPackage->isDev() && $prevPackage->getInstallationSource() === 'dist' && $package->isDev())
  332. ) {
  333. $prevSource = $prevPackage->getInstallationSource();
  334. usort($sources, function ($a, $b) use ($prevSource) {
  335. return $a === $prevSource ? -1 : 1;
  336. });
  337. return $sources;
  338. }
  339. // reverse sources in case dist is the preferred source for this package
  340. if (!$this->preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
  341. $sources = array_reverse($sources);
  342. }
  343. return $sources;
  344. }
  345. }