288 lines
9.3 KiB
PHP
288 lines
9.3 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Symfony package.
|
|
*
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Symfony\Flex;
|
|
|
|
use Composer\Config;
|
|
use Composer\Downloader\TransportException;
|
|
use Composer\IO\IOInterface;
|
|
use Composer\Util\RemoteFilesystem;
|
|
|
|
/**
|
|
* Speedup Composer by downloading packages in parallel.
|
|
*
|
|
* @author Nicolas Grekas <p@tchwork.com>
|
|
*/
|
|
class ParallelDownloader extends RemoteFilesystem
|
|
{
|
|
private $io;
|
|
private $downloader;
|
|
private $quiet = true;
|
|
private $progress = true;
|
|
private $nextCallback;
|
|
private $downloadCount;
|
|
private $nextOptions = [];
|
|
private $sharedState;
|
|
private $fileName;
|
|
private $lastHeaders;
|
|
|
|
public static $cacheNext = false;
|
|
protected static $cache = [];
|
|
|
|
public function __construct(IOInterface $io, Config $config, array $options = [], $disableTls = false)
|
|
{
|
|
$this->io = $io;
|
|
if (!method_exists(parent::class, 'getRemoteContents')) {
|
|
$this->io->writeError('Composer >=1.7 not found, downloads will happen in sequence', true, IOInterface::DEBUG);
|
|
} elseif (!\extension_loaded('curl')) {
|
|
$this->io->writeError('ext-curl not found, downloads will happen in sequence', true, IOInterface::DEBUG);
|
|
} else {
|
|
$this->downloader = new CurlDownloader();
|
|
}
|
|
parent::__construct($io, $config, $options, $disableTls);
|
|
}
|
|
|
|
public function download(array &$nextArgs, callable $nextCallback, bool $quiet = true, bool $progress = true)
|
|
{
|
|
$previousState = [$this->quiet, $this->progress, $this->downloadCount, $this->nextCallback, $this->sharedState];
|
|
$this->quiet = $quiet;
|
|
$this->progress = $progress;
|
|
$this->downloadCount = \count($nextArgs);
|
|
$this->nextCallback = $nextCallback;
|
|
$this->sharedState = (object) [
|
|
'bytesMaxCount' => 0,
|
|
'bytesMax' => 0,
|
|
'bytesTransferred' => 0,
|
|
'nextArgs' => &$nextArgs,
|
|
'nestingLevel' => 0,
|
|
'maxNestingReached' => false,
|
|
'lastProgress' => 0,
|
|
'lastUpdate' => microtime(true),
|
|
];
|
|
|
|
if (!$this->quiet) {
|
|
if (!$this->downloader && method_exists(parent::class, 'getRemoteContents')) {
|
|
$this->io->writeError('<warning>Enable the "cURL" PHP extension for faster downloads</>');
|
|
}
|
|
|
|
$note = '';
|
|
if ($this->io->isDecorated()) {
|
|
$note = '\\' === \DIRECTORY_SEPARATOR ? '' : (false !== stripos(\PHP_OS, 'darwin') ? '🎵' : '🎶');
|
|
$note .= $this->downloader ? ('\\' !== \DIRECTORY_SEPARATOR ? ' 💨' : '') : '';
|
|
}
|
|
|
|
$this->io->writeError('');
|
|
$this->io->writeError(sprintf('<info>Prefetching %d packages</> %s', $this->downloadCount, $note));
|
|
$this->io->writeError(' - Downloading', false);
|
|
if ($this->progress) {
|
|
$this->io->writeError(' (<comment>0%</>)', false);
|
|
}
|
|
}
|
|
try {
|
|
$this->getNext();
|
|
if ($this->quiet) {
|
|
// no-op
|
|
} elseif ($this->progress) {
|
|
$this->io->overwriteError(' (<comment>100%</>)');
|
|
} else {
|
|
$this->io->writeError(' (<comment>100%</>)');
|
|
}
|
|
} finally {
|
|
if (!$this->quiet) {
|
|
$this->io->writeError('');
|
|
}
|
|
list($this->quiet, $this->progress, $this->downloadCount, $this->nextCallback, $this->sharedState) = $previousState;
|
|
}
|
|
}
|
|
|
|
public function getOptions()
|
|
{
|
|
$options = array_replace_recursive(parent::getOptions(), $this->nextOptions);
|
|
$this->nextOptions = [];
|
|
|
|
return $options;
|
|
}
|
|
|
|
public function setNextOptions(array $options)
|
|
{
|
|
$this->nextOptions = parent::getOptions() !== $options ? $options : [];
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getLastHeaders()
|
|
{
|
|
return $this->lastHeaders ?? parent::getLastHeaders();
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function copy($originUrl, $fileUrl, $fileName, $progress = true, $options = [])
|
|
{
|
|
$options = array_replace_recursive($this->nextOptions, $options);
|
|
$this->nextOptions = [];
|
|
$rfs = clone $this;
|
|
$rfs->fileName = $fileName;
|
|
$rfs->progress = $this->progress && $progress;
|
|
|
|
try {
|
|
return $rfs->get($originUrl, $fileUrl, $options, $fileName, $rfs->progress);
|
|
} finally {
|
|
$rfs->lastHeaders = null;
|
|
$this->lastHeaders = $rfs->getLastHeaders();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public function getContents($originUrl, $fileUrl, $progress = true, $options = [])
|
|
{
|
|
return $this->copy($originUrl, $fileUrl, null, $progress, $options);
|
|
}
|
|
|
|
/**
|
|
* @internal
|
|
*/
|
|
public function callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax, $nativeDownload = true)
|
|
{
|
|
if (!$nativeDownload && \STREAM_NOTIFY_SEVERITY_ERR === $severity) {
|
|
throw new TransportException($message, $messageCode);
|
|
}
|
|
|
|
parent::callbackGet($notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax);
|
|
|
|
if (!$state = $this->sharedState) {
|
|
return;
|
|
}
|
|
|
|
if (\STREAM_NOTIFY_FILE_SIZE_IS === $notificationCode) {
|
|
++$state->bytesMaxCount;
|
|
$state->bytesMax += $bytesMax;
|
|
}
|
|
|
|
if (!$bytesMax || \STREAM_NOTIFY_PROGRESS !== $notificationCode) {
|
|
if ($state->nextArgs && !$nativeDownload) {
|
|
$this->getNext();
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (0 < $state->bytesMax) {
|
|
$progress = $state->bytesMaxCount / $this->downloadCount;
|
|
$progress *= 100 * ($state->bytesTransferred + $bytesTransferred) / $state->bytesMax;
|
|
} else {
|
|
$progress = 0;
|
|
}
|
|
|
|
if ($bytesTransferred === $bytesMax) {
|
|
$state->bytesTransferred += $bytesMax;
|
|
}
|
|
|
|
if (null !== $state->nextArgs && !$this->quiet && $this->progress && 1 <= $progress - $state->lastProgress) {
|
|
$progressTime = microtime(true);
|
|
|
|
if (5 <= $progress - $state->lastProgress || 1 <= $progressTime - $state->lastUpdate) {
|
|
$state->lastProgress = $progress;
|
|
$this->io->overwriteError(sprintf(' (<comment>%d%%</>)', $progress), false);
|
|
$state->lastUpdate = microtime(true);
|
|
}
|
|
}
|
|
|
|
if (!$nativeDownload || !$state->nextArgs || $bytesTransferred === $bytesMax || $state->maxNestingReached) {
|
|
return;
|
|
}
|
|
|
|
if (5 < $state->nestingLevel) {
|
|
$state->maxNestingReached = true;
|
|
} else {
|
|
$this->getNext();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
protected function getRemoteContents($originUrl, $fileUrl, $context, array &$responseHeaders = null, $maxFileSize = null)
|
|
{
|
|
if (isset(self::$cache[$fileUrl])) {
|
|
self::$cacheNext = false;
|
|
|
|
$result = self::$cache[$fileUrl];
|
|
|
|
if (3 < \func_num_args()) {
|
|
list($responseHeaders, $result) = $result;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
if (self::$cacheNext) {
|
|
self::$cacheNext = false;
|
|
|
|
if (3 < \func_num_args()) {
|
|
$result = $this->getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders, $maxFileSize);
|
|
self::$cache[$fileUrl] = [$responseHeaders, $result];
|
|
} else {
|
|
$result = $this->getRemoteContents($originUrl, $fileUrl, $context);
|
|
self::$cache[$fileUrl] = $result;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
if (!$this->downloader || !preg_match('/^https?:/', $fileUrl)) {
|
|
return parent::getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders, $maxFileSize);
|
|
}
|
|
|
|
try {
|
|
$result = $this->downloader->get($originUrl, $fileUrl, $context, $this->fileName);
|
|
|
|
if (3 < \func_num_args()) {
|
|
list($responseHeaders, $result) = $result;
|
|
}
|
|
|
|
return $result;
|
|
} catch (TransportException $e) {
|
|
$this->io->writeError('Retrying download: '.$e->getMessage(), true, IOInterface::DEBUG);
|
|
|
|
return parent::getRemoteContents($originUrl, $fileUrl, $context, $responseHeaders, $maxFileSize);
|
|
} catch (\Throwable $e) {
|
|
$responseHeaders = [];
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
private function getNext()
|
|
{
|
|
$state = $this->sharedState;
|
|
++$state->nestingLevel;
|
|
|
|
try {
|
|
while ($state->nextArgs && (!$state->maxNestingReached || 1 === $state->nestingLevel)) {
|
|
try {
|
|
$state->maxNestingReached = false;
|
|
($this->nextCallback)(...array_shift($state->nextArgs));
|
|
} catch (TransportException $e) {
|
|
$this->io->writeError('Skipping download: '.$e->getMessage(), true, IOInterface::DEBUG);
|
|
}
|
|
}
|
|
} finally {
|
|
--$state->nestingLevel;
|
|
}
|
|
}
|
|
}
|