DownloadManager.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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 installer downloader for a specific installation type.
  78. *
  79. * @param string $type installation type
  80. * @param DownloaderInterface $downloader downloader instance
  81. * @return DownloadManager
  82. */
  83. public function setDownloader($type, DownloaderInterface $downloader)
  84. {
  85. $type = strtolower($type);
  86. $this->downloaders[$type] = $downloader;
  87. return $this;
  88. }
  89. /**
  90. * Returns downloader for a specific installation type.
  91. *
  92. * @param string $type installation type
  93. * @throws \InvalidArgumentException if downloader for provided type is not registered
  94. * @return DownloaderInterface
  95. */
  96. public function getDownloader($type)
  97. {
  98. $type = strtolower($type);
  99. if (!isset($this->downloaders[$type])) {
  100. throw new \InvalidArgumentException(sprintf('Unknown downloader type: %s. Available types: %s.', $type, implode(', ', array_keys($this->downloaders))));
  101. }
  102. return $this->downloaders[$type];
  103. }
  104. /**
  105. * Returns downloader for already installed package.
  106. *
  107. * @param PackageInterface $package package instance
  108. * @throws \InvalidArgumentException if package has no installation source specified
  109. * @throws \LogicException if specific downloader used to load package with
  110. * wrong type
  111. * @return DownloaderInterface|null
  112. */
  113. public function getDownloaderForPackage(PackageInterface $package)
  114. {
  115. $installationSource = $package->getInstallationSource();
  116. if ('metapackage' === $package->getType()) {
  117. return;
  118. }
  119. if ('dist' === $installationSource) {
  120. $downloader = $this->getDownloader($package->getDistType());
  121. } elseif ('source' === $installationSource) {
  122. $downloader = $this->getDownloader($package->getSourceType());
  123. } else {
  124. throw new \InvalidArgumentException(
  125. 'Package '.$package.' does not have an installation source set'
  126. );
  127. }
  128. if ($installationSource !== $downloader->getInstallationSource()) {
  129. throw new \LogicException(sprintf(
  130. 'Downloader "%s" is a %s type downloader and can not be used to download %s for package %s',
  131. get_class($downloader),
  132. $downloader->getInstallationSource(),
  133. $installationSource,
  134. $package
  135. ));
  136. }
  137. return $downloader;
  138. }
  139. public function getDownloaderType(DownloaderInterface $downloader)
  140. {
  141. return array_search($downloader, $this->downloaders);
  142. }
  143. /**
  144. * Downloads package into target dir.
  145. *
  146. * @param PackageInterface $package package instance
  147. * @param string $targetDir target dir
  148. * @param PackageInterface|null $prevPackage previous package instance in case of updates
  149. *
  150. * @return PromiseInterface
  151. * @throws \InvalidArgumentException if package have no urls to download from
  152. * @throws \RuntimeException
  153. */
  154. public function download(PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
  155. {
  156. $this->filesystem->ensureDirectoryExists(dirname($targetDir));
  157. $sources = $this->getAvailableSources($package, $prevPackage);
  158. $io = $this->io;
  159. $self = $this;
  160. $download = function ($retry = false) use (&$sources, $io, $package, $self, $targetDir, &$download, $prevPackage) {
  161. $source = array_shift($sources);
  162. if ($retry) {
  163. $io->writeError(' <warning>Now trying to download from ' . $source . '</warning>');
  164. }
  165. $package->setInstallationSource($source);
  166. $downloader = $self->getDownloaderForPackage($package);
  167. if (!$downloader) {
  168. return \React\Promise\resolve();
  169. }
  170. $handleError = function ($e) use ($sources, $source, $package, $io, $download) {
  171. if ($e instanceof \RuntimeException) {
  172. if (!$sources) {
  173. throw $e;
  174. }
  175. $io->writeError(
  176. ' <warning>Failed to download '.
  177. $package->getPrettyName().
  178. ' from ' . $source . ': '.
  179. $e->getMessage().'</warning>'
  180. );
  181. return $download(true);
  182. }
  183. throw $e;
  184. };
  185. try {
  186. $result = $downloader->download($package, $targetDir, $prevPackage);
  187. } catch (\Exception $e) {
  188. return $handleError($e);
  189. }
  190. if (!$result instanceof PromiseInterface) {
  191. return \React\Promise\resolve($result);
  192. }
  193. $res = $result->then(function ($res) {
  194. return $res;
  195. }, $handleError);
  196. return $res;
  197. };
  198. return $download();
  199. }
  200. /**
  201. * Prepares an operation execution
  202. *
  203. * @param string $type one of install/update/uninstall
  204. * @param PackageInterface $package package instance
  205. * @param string $targetDir target dir
  206. * @param PackageInterface|null $prevPackage previous package instance in case of updates
  207. *
  208. * @return PromiseInterface|null
  209. */
  210. public function prepare($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
  211. {
  212. $downloader = $this->getDownloaderForPackage($package);
  213. if ($downloader) {
  214. return $downloader->prepare($type, $package, $targetDir, $prevPackage);
  215. }
  216. }
  217. /**
  218. * Installs package into target dir.
  219. *
  220. * @param PackageInterface $package package instance
  221. * @param string $targetDir target dir
  222. *
  223. * @return PromiseInterface|null
  224. * @throws \InvalidArgumentException if package have no urls to download from
  225. * @throws \RuntimeException
  226. */
  227. public function install(PackageInterface $package, $targetDir)
  228. {
  229. $downloader = $this->getDownloaderForPackage($package);
  230. if ($downloader) {
  231. return $downloader->install($package, $targetDir);
  232. }
  233. }
  234. /**
  235. * Updates package from initial to target version.
  236. *
  237. * @param PackageInterface $initial initial package version
  238. * @param PackageInterface $target target package version
  239. * @param string $targetDir target dir
  240. *
  241. * @return PromiseInterface|null
  242. * @throws \InvalidArgumentException if initial package is not installed
  243. */
  244. public function update(PackageInterface $initial, PackageInterface $target, $targetDir)
  245. {
  246. $downloader = $this->getDownloaderForPackage($target);
  247. $initialDownloader = $this->getDownloaderForPackage($initial);
  248. // no downloaders present means update from metapackage to metapackage, nothing to do
  249. if (!$initialDownloader && !$downloader) {
  250. return;
  251. }
  252. // if we have a downloader present before, but not after, the package became a metapackage and its files should be removed
  253. if (!$downloader) {
  254. return $initialDownloader->remove($initial, $targetDir);
  255. }
  256. $initialType = $this->getDownloaderType($initialDownloader);
  257. $targetType = $this->getDownloaderType($downloader);
  258. if ($initialType === $targetType) {
  259. try {
  260. return $downloader->update($initial, $target, $targetDir);
  261. } catch (\RuntimeException $e) {
  262. if (!$this->io->isInteractive()) {
  263. throw $e;
  264. }
  265. $this->io->writeError('<error> Update failed ('.$e->getMessage().')</error>');
  266. if (!$this->io->askConfirmation(' Would you like to try reinstalling the package instead [<comment>yes</comment>]? ', true)) {
  267. throw $e;
  268. }
  269. }
  270. }
  271. // if downloader type changed, or update failed and user asks for reinstall,
  272. // we wipe the dir and do a new install instead of updating it
  273. $promise = $initialDownloader->remove($initial, $targetDir);
  274. if ($promise) {
  275. $self = $this;
  276. return $promise->then(function ($res) use ($self, $target, $targetDir) {
  277. return $self->install($target, $targetDir);
  278. });
  279. }
  280. return $this->install($target, $targetDir);
  281. }
  282. /**
  283. * Removes package from target dir.
  284. *
  285. * @param PackageInterface $package package instance
  286. * @param string $targetDir target dir
  287. *
  288. * @return PromiseInterface|null
  289. */
  290. public function remove(PackageInterface $package, $targetDir)
  291. {
  292. $downloader = $this->getDownloaderForPackage($package);
  293. if ($downloader) {
  294. return $downloader->remove($package, $targetDir);
  295. }
  296. }
  297. /**
  298. * Cleans up a failed operation
  299. *
  300. * @param string $type one of install/update/uninstall
  301. * @param PackageInterface $package package instance
  302. * @param string $targetDir target dir
  303. * @param PackageInterface|null $prevPackage previous package instance in case of updates
  304. *
  305. * @return PromiseInterface|null
  306. */
  307. public function cleanup($type, PackageInterface $package, $targetDir, PackageInterface $prevPackage = null)
  308. {
  309. $downloader = $this->getDownloaderForPackage($package);
  310. if ($downloader) {
  311. return $downloader->cleanup($type, $package, $targetDir, $prevPackage);
  312. }
  313. }
  314. /**
  315. * Determines the install preference of a package
  316. *
  317. * @param PackageInterface $package package instance
  318. *
  319. * @return string
  320. */
  321. protected function resolvePackageInstallPreference(PackageInterface $package)
  322. {
  323. foreach ($this->packagePreferences as $pattern => $preference) {
  324. $pattern = '{^'.str_replace('\\*', '.*', preg_quote($pattern)).'$}i';
  325. if (preg_match($pattern, $package->getName())) {
  326. if ('dist' === $preference || (!$package->isDev() && 'auto' === $preference)) {
  327. return 'dist';
  328. }
  329. return 'source';
  330. }
  331. }
  332. return $package->isDev() ? 'source' : 'dist';
  333. }
  334. /**
  335. * @return string[]
  336. */
  337. private function getAvailableSources(PackageInterface $package, PackageInterface $prevPackage = null)
  338. {
  339. $sourceType = $package->getSourceType();
  340. $distType = $package->getDistType();
  341. // add source before dist by default
  342. $sources = array();
  343. if ($sourceType) {
  344. $sources[] = 'source';
  345. }
  346. if ($distType) {
  347. $sources[] = 'dist';
  348. }
  349. if (empty($sources)) {
  350. throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
  351. }
  352. if (
  353. $prevPackage
  354. // if we are updating, we want to keep the same source as the previously installed package (if available in the new one)
  355. && in_array($prevPackage->getInstallationSource(), $sources, true)
  356. // unless the previous package was stable dist (by default) and the new package is dev, then we allow the new default to take over
  357. && !(!$prevPackage->isDev() && $prevPackage->getInstallationSource() === 'dist' && $package->isDev())
  358. ) {
  359. $prevSource = $prevPackage->getInstallationSource();
  360. usort($sources, function ($a, $b) use ($prevSource) {
  361. return $a === $prevSource ? -1 : 1;
  362. });
  363. return $sources;
  364. }
  365. // reverse sources in case dist is the preferred source for this package
  366. if (!$this->preferSource && ($this->preferDist || 'dist' === $this->resolvePackageInstallPreference($package))) {
  367. $sources = array_reverse($sources);
  368. }
  369. return $sources;
  370. }
  371. }