├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── src ├── Downloader.php ├── Downloader │ └── Aria.php ├── DownloaderFactory.php └── FastFetch.php └── tests └── example-usage └── composer.json /.gitattributes: -------------------------------------------------------------------------------- 1 | tests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | nbproject 2 | vendor -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: [7.0] #, 5.6, 5.5] 4 | 5 | before_install: 6 | - sudo apt-get update -qq 7 | - sudo apt-get install -y aria2 8 | - aria2c -v 9 | 10 | script: 11 | - cd tests/example-usage 12 | - composer install --no-dev --prefer-dist --optimize-autoloader --profile -vvv -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2015 Jens-André Koch 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Composer Plugin - FastFetch 2 | --------------------------- 3 | 4 | The Composer Plugin FastFetch downloads packages fast and in parallel into your Composer cache. 5 | 6 | [![Total Downloads](https://img.shields.io/packagist/dt/jakoch/composer-fastfetch.svg?style=flat-square)](https://packagist.org/packages/jakoch/composer-fastfetch) 7 | [![Latest Stable Version](https://img.shields.io/packagist/v/jakoch/composer-fastfetch.svg?style=flat-square)](https://packagist.org/packages/jakoch/composer-fastfetch) 8 | [![Build Status](https://img.shields.io/travis/jakoch/composer-fastfetch/master.svg?style=flat-square)](http://travis-ci.org/#!/jakoch/composer-fastfetch) 9 | [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/jakoch/composer-fastfetch/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/jakoch/composer-fastfetch/?branch=master) 10 | [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/jakoch/composer-fastfetch/master.svg?style=flat-square)](https://scrutinizer-ci.com/g/jakoch/composer-fastfetch/?branch=master) 11 | 12 | #### Requirements 13 | 14 | - **External Download Tool - Aria2** 15 | - This package requires an external downloading tool named "[aria2](https://aria2.github.io/)". 16 | - You may find the latest release over here: https://github.com/tatsuhiro-t/aria2/releases/latest 17 | - Linux, e.g. Travis: `sudo apt-get install -y aria2` 18 | - Windows: download manually and add it to the environment path or to the `\bin` folder of your project 19 | - Mac/OSX: download & compile or `brew install aria2` 20 | - **Env** 21 | - This plugin makes use of `exec()` to run the external download tool on the CLI. It will not work on shared hosting environments, where `exec` is disabled (`php.ini` `disabled_functions`). 22 | 23 | #### Installation and Usage 24 | 25 | - You might install this plugin on a single project or globally. 26 | 27 | - Use `--prefer-dist` when executing Composer, e.g. `php composer.phar install --prefer-dist`. 28 | 29 | ##### Installation for a single project (project install) 30 | 31 | - Add the package dependency `jakoch/composer-fastfetch` at the pole position of your require section. 32 | 33 | ``` 34 | { 35 | "require": { 36 | "jakoch/composer-fastfetch": "*", 37 | "vendor/package-A": "1.0.0", 38 | "vendor/package-B": "2.0.0" 39 | } 40 | } 41 | ``` 42 | 43 | #### Installation for multiple projects (global install) 44 | 45 | ##### Install 46 | 47 | ```bash 48 | $ composer global require jakoch/composer-fastfetch 49 | ``` 50 | 51 | ##### Uninstall 52 | 53 | ```bash 54 | $ composer global remove jakoch/composer-fastfetch 55 | ``` 56 | 57 | #### How does this work? 58 | 59 | FastFetch is a Composer Plugin for downloading packages fast and in parallel into your cache. 60 | 61 | Conceptually the plugin implements a simple cache warming strategy. 62 | 63 | When Composer reaches the install step, it will firstly download this plugin. 64 | The plugin will generate a list with download urls for the packages. 65 | Then it shell-executes a download tool. The preferred download tool is "aria2". 66 | You might also know it as the tool behind [apt-fast](https://github.com/ilikenwf/apt-fast). 67 | (Other tools like "curl", "wget" might also get supported in the future. Please send PRs.) 68 | 69 | The download tool retrieves all the packages directly into Composers cache folder. 70 | When Composer proceeds and reaches the "install/download" event for the next packages, 71 | it will skip downloading and use the cached files for installation. 72 | 73 | #### Workflow of the plugin 74 | 75 | This is a rough overview of the execution flow: 76 | 77 | - **Composer Init** 78 | - You execute `composer install --prefer-dist` 79 | - Composer hopefully resolves your dependencies into a stable set. 80 | - This stable set contains the dist URLs for all the packages. 81 | - **Composer Download Step** 82 | - Composer starts downloading the packages. 83 | - The first downloaded package is the plugin "composer-fastfetch". 84 | - The plugin is auto-started after its download. 85 | - **FastFetch Plugin** 86 | - Detect Download Tool on Environment and init command wrapper class 87 | - Build download list 88 | - (Transform Array of DistUrls into a file with the URLs to download) 89 | - Run download tool 90 | - (Process "downloads" file and download each file into the cache folder) 91 | - External Download tool fetched the files. All done. Exit Plugin. 92 | - **Back in the Composer Download Step** 93 | - Composer is still in the download step and proceeds as normal. 94 | It tries to downloading the next dependencies/packages, 95 | BUT skips each, because all the files are already in the Composer cache folder. 96 | - **Composer Install Step** 97 | - Composer extracts the packages from the cache to the vendor folder. 98 | 99 | ### Why? 100 | 101 | The main reason for this plugin is that Composer downloads all files one after another (sequentially). 102 | This is true for the package metadata (packagist.org) and all package downloads (dists). 103 | The downloads work, but the overall speed and download experience isn't that nice. 104 | 105 | Parallelizing the metadata retrival is complicated, because Composer doesn't expose events (like "onBeforeMetadataRetrieval" or similar) and the internal API is protected. At this point in time, its not possible to parallelize the metadata fetching. 106 | 107 | But, Composer provides an event "onPostDependenciesSolving", which means the Solver has finishes its dirty deeds 108 | and a stable set of packages has been determined. The set might contain download urls for the packages, which allows forwarding them to a parallel downloader. 109 | 110 | Aria makes multiple connections to one or multiple servers and reuses connections, when possible. 111 | Files are not downloaded sequentially, but in parallel. 112 | 113 | All in all, this might result in a slight download speed improvement and improve the download experience. 114 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jakoch/composer-fastfetch", 3 | "description": "Fastfetch - A Composer plugin downloading packages into the cache.", 4 | "keywords": ["composer", "download", "plugin", "package", "parallel", "async", "aria2", "curl", "wget"], 5 | "homepage": "https://github.com/jakoch/composer-fastfetch/", 6 | "type": "composer-plugin", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Jens A. Koch", 11 | "email": "jakoch@web.de", 12 | "homepage": "http://jakoch.de/" 13 | } 14 | ], 15 | "support": { 16 | "issues": "https://github.com/jakoch/composer-fastfetch/issues" 17 | }, 18 | "require": { 19 | "composer-plugin-api": "^1.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "^4.5", 23 | "composer/composer": "^1.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { "Composer\\Plugin\\FastFetch\\": "src/" } 27 | }, 28 | "autoload-dev": { 29 | "psr-4": { "Composer\\Plugin\\FastFetch\\Test\\": "tests/" } 30 | }, 31 | "extra": { 32 | "class": "Composer\\Plugin\\FastFetch\\FastFetch" 33 | }, 34 | "scripts": { 35 | "test": "phpunit" 36 | } 37 | } -------------------------------------------------------------------------------- /src/Downloader.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * The content is released under the MIT License. Please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Plugin\FastFetch; 13 | 14 | use Composer\Cache; 15 | use Composer\Config; 16 | use Composer\DependencyResolver\Operation\InstallOperation; 17 | use Composer\DependencyResolver\Operation\UpdateOperation; 18 | use Composer\IO\IOInterface; 19 | use Composer\Util\ProcessExecutor; 20 | use Composer\Package; 21 | //use Composer\Util\GitHub; // helper util to raise rate limit? 22 | 23 | /** 24 | * Downloader 25 | */ 26 | class Downloader 27 | { 28 | public $composer, $io, $cache, $config; 29 | 30 | public $cacheFilesDir = ''; 31 | 32 | /** 33 | * @var object Composer\Plugin\FastFetch\Downloader\* 34 | */ 35 | public $downloader; 36 | 37 | /** 38 | * @var string DownloadList 39 | */ 40 | public $downloadList = array(); 41 | 42 | /** 43 | * @var string Path to download list file. 44 | */ 45 | public $downloadFile; 46 | 47 | /** 48 | * @var string Path to bin-dir 49 | */ 50 | public $binDir = ''; 51 | 52 | public function __construct(\Composer\Composer $composer, \Composer\IO\IOInterface $io, $downloader) 53 | { 54 | $this->composer = $composer; 55 | $this->io = $io; 56 | $this->config = $composer->getConfig(); 57 | $this->downloader = $downloader; 58 | 59 | // exit early, if no downloader was found 60 | if (!is_object($this->downloader)) { 61 | $this->io->write('Skipping downloading, because Composer couldn\'t find a suitable download tool.'); 62 | $this->io->write('You might install one of the following tools: aria2'); //, wget, curl. 63 | return; 64 | } 65 | 66 | // init cache 67 | if ($this->config->get('cache-files-ttl') > 0) { 68 | $this->cacheFilesDir = $this->config->get('cache-files-dir'); 69 | $this->cache = new Cache($this->io, $this->cacheFilesDir, 'a-z0-9_./'); 70 | } 71 | 72 | // remove log file on re-run 73 | if ($this->io->isDebug()) { 74 | $this->removeLogfile(); 75 | } 76 | } 77 | 78 | /** 79 | * Main method for downloading. 80 | * 81 | * Execution workflow: 82 | * 83 | * 1. iterate over the Operations array 84 | * 2. fetch distURLs for all packages 85 | * 3. add URLs to a download list, which is stored to a temp file 86 | * 4. finally: execute the download util and process URL list 87 | * 88 | * @param array $operations 89 | * @return type 90 | */ 91 | public function downloadPackages(array $operations) 92 | { 93 | if ($this->io->isVerbose()) { 94 | $this->io->write('Adding packages for downloading....'); 95 | } 96 | 97 | $counter = 0; 98 | 99 | foreach ($operations as $idx => $op) { 100 | if ($op instanceof InstallOperation) { // package to install 101 | $package = $op->getPackage(); 102 | } elseif ($op instanceof UpdateOperation) { // package to update 103 | $package = $op->getTargetPackage(); 104 | } else { 105 | continue; // skip all other packages 106 | } 107 | 108 | // do not download this plugin again 109 | if($package->getName() === 'jakoch/composer-fastfetch') { 110 | continue; 111 | } 112 | 113 | // show package name 114 | if ($this->io->isVerbose()) { 115 | $this->io->write('[' . $package->getName() . ']'); // (' . VersionParser::formatVersion($package) . ')'); 116 | } 117 | 118 | // Lets grab the URLs of all packages (not-cached-yet) 119 | $url = $package->getDistUrl(); 120 | 121 | // uhm, mutiple URLs? are that real mirror URLs or local mirrors? 122 | //$url = array_shift($urls); 123 | 124 | // moan, when there is no download URL 125 | if (!$package->getDistUrl()) { 126 | if ($this->io->isVerbose()) { 127 | $this->io->write(' - The package was not added to the download list, because it did not provide any distUrls.'); 128 | } 129 | continue; 130 | } 131 | 132 | /** 133 | * The next section is similar to Composer/Downloader/FileDownloader->doDownload() 134 | * 135 | * Build cache infos: sum, key, (cached) file, full cache target path 136 | */ 137 | $checksum = $package->getDistSha1Checksum(); 138 | $fileName = $package->getTargetDir() . '/' . pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_BASENAME); 139 | $cacheFile = $this->getCacheKey($package); 140 | $cacheFolder = $this->cacheFilesDir . '/' . $cacheFile; 141 | 142 | /*var_dump( 143 | $package->getDistUrl(), 144 | $package->getDistUrls, 145 | 'Checksum: ' . $checksum, 146 | 'SHA1: ' . $this->cache->sha1($cacheFile) 147 | //'hashfile sha1: ' .hash_file('sha1', $fileName) 148 | ); exit;*/ 149 | 150 | /** 151 | * add distURL to download list, but only, if file is "not cached, yet" or "cache is invalidated". 152 | * 153 | * Note: to improve the Composer API, get rid of this long condition check, in favor of a short function, 154 | * e.g. $package->isCached() or $cache->hasPackage($package). 155 | * 156 | */ 157 | if (!$this->cache || ($checksum && $checksum !== $this->cache->sha1($cacheFile)) || !$this->cache->copyTo($cacheFile, $fileName)) { 158 | 159 | // show URL and the target path for this package 160 | if ($this->io->isVerbose()) { 161 | $this->io->write(' + From ' . $url); 162 | $this->io->write(' To ' . $cacheFolder); 163 | } 164 | 165 | $this->downloader->addDownloadUrl($url, $cacheFolder); 166 | 167 | $counter++; 168 | } else { 169 | if ($this->io->isVerbose()) { 170 | $this->io->write(' - The package was not added to the download list, because its already cached.'); 171 | } 172 | } 173 | } 174 | 175 | // skip parallel downloading, if "no distUrls found". 176 | if($counter == 0) { 177 | $this->io->write('FastFetch: skipped downloading, because no distUrls were found.' . PHP_EOL); 178 | return; 179 | } 180 | 181 | // save download list to temp folder 182 | $this->downloadsFile = tempnam(sys_get_temp_dir(), 'download.list'); 183 | file_put_contents($this->downloadsFile, $this->downloader->downloadList); 184 | 185 | $this->io->write('Downloading ' . $counter . ' packages.'); 186 | $this->io->write('Working... this may take a while.'); 187 | $this->executeDownloader(); 188 | $this->io->write('FastFetch: Done.' . PHP_EOL); 189 | 190 | unlink($this->downloadsFile); 191 | } 192 | 193 | public function executeDownloader() 194 | { 195 | // collect output into a buffer, but only display in verbose mode 196 | $callableOutputHandler = function ($type, $buffer) use (&$output) { 197 | if ($type !== 'out') { 198 | return; 199 | } 200 | $output .= $buffer; 201 | if ($this->io->isVerbose()) { 202 | $this->io->write($buffer, false); 203 | } 204 | }; 205 | 206 | $this->process = new ProcessExecutor($this->io); 207 | // use command from downloader, insert into command string, then execute 208 | $command = $this->downloader->getCommand(); 209 | $command = ($this->binDir !== '') ? $this->binDir . $command : $command; 210 | $escapedCmd = sprintf($command, $this->downloadsFile); 211 | 212 | if (!0 === $this->process->execute($escapedCmd, $callableOutputHandler)) { 213 | throw new \RuntimeException('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput()); 214 | } 215 | } 216 | 217 | /** 218 | * Get the cache file path (without cache-files-dir prefix). 219 | * 220 | * @param PackageInterface $p 221 | * @return type 222 | */ 223 | public function getCacheKey($p) 224 | { 225 | $distRef = $p->getDistReference(); 226 | 227 | if (preg_match('{^[a-f0-9]{40}$}', $distRef)) { 228 | return $p->getName() . '/' . $distRef . '.' . $p->getDistType(); 229 | } 230 | 231 | // Composer builds the key like this, but it doesn't work: 232 | //return $p->getName().'/'.$p->getVersion().'-'.$distRef.'.'.$p->getDistType(); 233 | 234 | // we need to apply the SHA1 fix from 235 | // https://github.com/krispypen/composer/commit/d8fa9ab57efdac94cd7135d96c2523932ac30f8b 236 | return $p->getName().'/'.$p->getVersion().'-'.$distRef.'-'.$p->getDistSha1Checksum().'.'.$p->getDistType(); 237 | } 238 | 239 | /** 240 | * Returns the authentication token for the url. 241 | * 242 | * @param string $url 243 | * @return string AuthToken 244 | */ 245 | public function getAuthTokenForUrl($url) 246 | { 247 | if (false !== strpos($url, 'github')) { 248 | if ($oauth = $this->config->get('github-oauth')) { 249 | foreach ($oauth as $domain => $token) { 250 | if(false !== strpos($url, $domain)) { 251 | return $token; 252 | } 253 | } 254 | } 255 | } 256 | } 257 | 258 | public function removeLogfile() 259 | { 260 | $logfile = getcwd() . '/composer.fastfetch.log'; 261 | 262 | if (file_exists($logfile)) { 263 | unlink($logfile); 264 | } 265 | } 266 | 267 | } 268 | -------------------------------------------------------------------------------- /src/Downloader/Aria.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * The content is released under the MIT License. Please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Plugin\FastFetch\Downloader; 13 | 14 | use Composer\IO\IOInterface; 15 | use Composer\Composer; 16 | 17 | /** 18 | * A wrapper class for the download tool "aria2". 19 | */ 20 | class Aria 21 | { 22 | // @var IOInterface 23 | protected $composer; 24 | 25 | // @var IOInterface 26 | protected $io; 27 | 28 | public $downloadList = ''; 29 | 30 | public function __construct(\Composer\Composer $composer, \Composer\IO\IOInterface $io) 31 | { 32 | $this->composer = $composer; 33 | $this->io = $io; 34 | 35 | $io->write('Using downloader: Aria'); 36 | } 37 | 38 | public function addDownloadUrl($sources, $target) 39 | { 40 | $sources = (array) $sources; 41 | 42 | // can a package have multiple download sources (mirrors)? 43 | foreach ($sources as $idx => $url) { 44 | $this->downloadList .= $url . PHP_EOL; 45 | 46 | /** 47 | * @todo bypass the github rate limit by setting a authentication header per URI 48 | * 49 | * setting an authentication header per URI? 50 | * this feature is not (yet) supported by aria2, 51 | * see https://github.com/tatsuhiro-t/aria2/issues/81 52 | */ 53 | //if ($token = $this->getAuthTokenForUrl($url)) { 54 | // $this->downloadList .= ' header="Authorization: token ' . $token . '"' . PHP_EOL; 55 | //} 56 | } 57 | 58 | $this->downloadList .= ' dir=' . pathinfo($target, PATHINFO_DIRNAME) . PHP_EOL; 59 | $this->downloadList .= ' out=' . pathinfo($target, PATHINFO_BASENAME) . PHP_EOL; 60 | } 61 | 62 | /** 63 | * getCommand returns a sprintf() string to invoke "aria2c". 64 | * The download file is inserted later inserted at "-i%s". 65 | * The rest of the arguments are configuration and auth, see: 66 | * 67 | * Input File Options: 68 | * @link http://aria2.sourceforge.net/manual/en/html/aria2c.html#input-file 69 | * 70 | * Options used: 71 | * -i Download the files listed in this file concurrently. 72 | * -s Specifies the number of simultaneous connections per host. 73 | * Raised from 2 to 4 (ignoring RFC 2616). 74 | * -j Specifies the number of concurrent downloads. 75 | * -x Max. number of connections to one server for each download. 76 | * -t Timeout in seconds. 77 | * -l Logs to file. 78 | */ 79 | public function getCommand() 80 | { 81 | $cmd = 'aria2c' 82 | . ' --conditional-get=true --auto-file-renaming=false' 83 | . ' --allow-overwrite=true --http-accept-gzip=true' 84 | . ' --enable-color=true --check-certificate=false' 85 | . ' --input-file=%s -s4 -j4 -x2 -t10'; // rant! 86 | // . ' --user-agent="' . $this->getUserAgent() . '"'; 87 | 88 | //if($this->io->isDebug()) { 89 | // $cmd .= ' -lcomposer.fastfetch.log'; 90 | //} 91 | 92 | // setting the auth header token for Github might be useful to raise the download limit? 93 | // X-RateLimit-Limit 94 | //$cmd .= ' --header="Authorization: token ' . $this->getAuthTokenForUrl('github.com') . '"'; 95 | 96 | return $cmd; 97 | } 98 | 99 | public function getUserAgent() 100 | { 101 | $composerVersion = (Composer::VERSION === '@package_version@') ? 'source' : Composer::VERSION; 102 | 103 | $phpVersion = (defined('HHVM_VERSION')) 104 | ? 'HHVM ' . HHVM_VERSION 105 | : 'PHP ' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION . '.' . PHP_RELEASE_VERSION; 106 | 107 | return sprintf('Composer/%s (%s; %s; %s)', $composerVersion, php_uname('s'), php_uname('r'), $phpVersion); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/DownloaderFactory.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * The content is released under the MIT License. Please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Plugin\FastFetch; 13 | 14 | use Composer\Util\ProcessExecutor; 15 | 16 | /** 17 | * 18 | */ 19 | class DownloaderFactory 20 | { 21 | public $io; 22 | public $composer; 23 | public $binPath = false; 24 | 25 | public function __construct(\Composer\Composer $composer, \Composer\IO\IOInterface $io) 26 | { 27 | $this->composer = $composer; 28 | $this->io = $io; 29 | $this->config = $composer->getConfig(); 30 | } 31 | 32 | /** 33 | * Searches the environment for known downloaders 34 | * and returns the (cli-command wrapper) object for the downloader. 35 | * Its a factory method for download util wrappers. 36 | * 37 | * The "return early style" defines the downloader priority: 38 | * 39 | * 1. config->get('extra/composer-fastfetch/download-tool') 40 | * 2. aria2c 41 | * 3. parallel + curl 42 | * 4. curl 43 | * 5. parallel + wget 44 | * 6. wget 45 | * 46 | * @return string Classname of the downloader. 47 | */ 48 | public function getDownloader() 49 | { 50 | /* 51 | // override download tool detection, by using the manually defined tool from config 52 | if ($this->config->get('downloader')) { 53 | $name = $this->config->get('downloader'); 54 | $className = 'Downloader\\' . $name; 55 | return new $className($this->composer, $this->io); 56 | } 57 | */ 58 | if ($this->io->isVerbose()) { 59 | $this->io->write('Searching environment for known download tools...'); 60 | } 61 | 62 | if ($this->cmdExists('aria2c')) { 63 | return new Downloader\Aria($this->composer, $this->io, $this->binPath); 64 | } 65 | /* 66 | if ($this->cmdExists('curl')) { 67 | if ($this->cmdExists('parallel')) { 68 | return new Downloader\ParallelCurl($this->composer, $this->io); 69 | } 70 | return new Downloader\Curl($this->composer, $this->io); 71 | } 72 | 73 | if ($this->cmdExists('wget')) { 74 | if ($this->cmdExists('parallel')) { 75 | return new Downloader\ParallelWget($this->composer, $this->io); 76 | } 77 | return new Downloader\Wget($this->composer, $this->io); 78 | } 79 | */ 80 | return false; 81 | } 82 | 83 | /** 84 | * Check, if a command exists on the environment. 85 | * 86 | * @param string $command The command to look for. 87 | * @return bool True, if the command has been found. False, otherwise. 88 | */ 89 | public function cmdExists($command) 90 | { 91 | $binary = (PHP_OS === 'WINNT') ? $command . '.exe' : $command; 92 | 93 | // check, if file exists in the "bin-dir" 94 | $binDir = $this->config->get('bin-dir') . DIRECTORY_SEPARATOR; 95 | $file = $binDir . $binary; 96 | if(file_exists($file)) { 97 | $this->binPath = $file; 98 | return true; 99 | } 100 | 101 | // check, if the file can be detected on the ENV using "where" or "which" 102 | $find = (PHP_OS === 'WINNT') ? 'where' : 'which'; 103 | $cmd = sprintf('%s %s', escapeshellarg($find), escapeshellarg($command)); 104 | $process = new ProcessExecutor($this->io); 105 | return ($process->execute($cmd) === 0); 106 | } 107 | } -------------------------------------------------------------------------------- /src/FastFetch.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * The content is released under the MIT License. Please view 9 | * the LICENSE file that was distributed with this source code. 10 | */ 11 | 12 | namespace Composer\Plugin\FastFetch; 13 | 14 | use Composer\Composer; 15 | use Composer\EventDispatcher\Event; 16 | use Composer\EventDispatcher\EventSubscriberInterface; 17 | use Composer\IO\IOInterface; 18 | use Composer\Plugin\FastFetch\Downloader; 19 | use Composer\Plugin\PluginEvents; 20 | use Composer\Plugin\PluginInterface; 21 | 22 | /** 23 | * FastFetch is a Composer Plugin for downloading packages fast and in parallel into your cache. 24 | * 25 | * When Composer reaches the install step, it will firstly download this plugin. 26 | * The plugin generates a list with download urls for the packages 27 | * and shell-executes a download tool. The preferred download tool is "aria2". 28 | * The download tool retrieves all the packages directly into Composers cache folder. 29 | * When Composer proceeds and reaches the "install/download" event for the next packages, 30 | * it will skip downloading and use the cached files for installation. 31 | */ 32 | class FastFetch implements PluginInterface, EventSubscriberInterface 33 | { 34 | // @var Composer 35 | protected $composer; 36 | 37 | // @var IOInterface 38 | protected $io; 39 | 40 | protected $tool; 41 | 42 | protected $downloader; 43 | 44 | /** 45 | * {@inheritDoc} 46 | */ 47 | public function activate(\Composer\Composer $composer,\Composer\IO\IOInterface $io) 48 | { 49 | $this->composer = $composer; 50 | $this->io = $io; 51 | 52 | $factory = new DownloaderFactory($composer, $io); 53 | $this->tool = $factory->getDownloader(); 54 | 55 | $this->downloader = new Downloader($composer, $io, $this->tool); 56 | 57 | /** 58 | * @todo if we could get all distUrls from Composer at this position, 59 | * we could skip the event subscribing and proceed executing the downloader. 60 | * But $operations comes directly from the Solver and vanishes in the event system. 61 | * https://github.com/composer/composer/blob/9e9c1917e1ed9f3f78b195a785aee3c6dc3cb883/src/Composer/Installer.php#L523 62 | */ 63 | // exit early, if no downloader was found 64 | /*if ($tool === false) { 65 | $this->io->write('Skipping downloading, because Composer couldn\'t find a suitable download tool.'); 66 | $this->io->write('You might install one of the following tools: aria2'); //, wget, curl. 67 | return; 68 | }*/ 69 | //$this->downloader->downloadPackages( $composer->fromWhichObject()->getOperations() ); 70 | } 71 | 72 | /** 73 | * @inheritdoc 74 | */ 75 | public static function getSubscribedEvents() 76 | { 77 | return [ 78 | 'post-package-install' => 'onPostPackageInstall', 79 | 'post-package-update' => 'onPostPackageUpdate', 80 | ]; 81 | } 82 | 83 | public function onPostPackageInstall(\Composer\Installer\PackageEvent $event) 84 | { 85 | $this->downloadPackages($event); 86 | } 87 | 88 | public function onPostPackageUpdate(\Composer\Installer\PackageEvent $event) 89 | { 90 | $this->downloadPackages($event); 91 | } 92 | 93 | public function downloadPackages(\Composer\Installer\PackageEvent $event) 94 | { 95 | // exit early, if no downloader was found 96 | if ($this->tool === false) { 97 | $this->io->write('Skipping downloading, because Composer couldn\'t find a suitable download tool.'); 98 | $this->io->write('You might install one of the following tools: aria2'); //, wget, curl. 99 | return; 100 | } 101 | 102 | $installedPackage = $event->getOperation()->getPackage(); 103 | 104 | if('jakoch/composer-fastfetch' === $installedPackage->getPrettyName()) { 105 | $this->downloader->downloadPackages( $event->getOperations() ); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /tests/example-usage/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "jakoch/composer-fastfetch": "*@dev", 4 | "ksst/kf": "0.1.31" 5 | }, 6 | "minimum-stability": "dev", 7 | "prefer-stable": true 8 | } --------------------------------------------------------------------------------