├── public └── .gitignore ├── resources ├── public │ ├── favicon.ico │ ├── apple-touch-icon.png │ ├── touch-icon-192x192.png │ ├── robots.txt │ └── apple-touch-icon-precomposed.png └── index.html.php ├── .editorconfig ├── phpunit.xml ├── src ├── ShortName.php ├── Console.php ├── IProgressBar.php ├── GZip.php ├── IO.php ├── Mirror.php ├── ProgressBar.php ├── Package.php ├── Provider.php ├── Command │ ├── Base.php │ ├── Clean.php │ └── Create.php ├── Http.php └── Filesystem.php ├── .env.example ├── LICENSE ├── composer.json ├── .circleci └── config.yml └── bin └── mirror /public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /resources/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnpkg/packagist-mirror/HEAD/resources/public/favicon.ico -------------------------------------------------------------------------------- /resources/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnpkg/packagist-mirror/HEAD/resources/public/apple-touch-icon.png -------------------------------------------------------------------------------- /resources/public/touch-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnpkg/packagist-mirror/HEAD/resources/public/touch-icon-192x192.png -------------------------------------------------------------------------------- /resources/public/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | # www.google.com/support/webmasters/bin/answer.py?hl=en&answer=156449 3 | 4 | User-agent: * 5 | -------------------------------------------------------------------------------- /resources/public/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cnpkg/packagist-mirror/HEAD/resources/public/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests 6 | 7 | 8 | 9 | 10 | src/ 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ShortName.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | trait ShortName 20 | { 21 | /** 22 | * Find hash and replace by *. 23 | * 24 | * @param string $name Name of provider or package 25 | * 26 | * @return string Shortname 27 | */ 28 | protected function shortname(string $name):string 29 | { 30 | return str_replace('*', '$*', preg_replace('/\$(\w*)/', '*', $name)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Console.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | trait Console 23 | { 24 | /** 25 | * @var InputInterface 26 | */ 27 | protected $input; 28 | 29 | /** 30 | * @var OutputInterface 31 | */ 32 | protected $output; 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function setConsole(InputInterface $input, OutputInterface $output):void 38 | { 39 | $this->input = $input; 40 | $this->output = $output; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Application name 2 | APP_NAME='Packagist Mirror' 3 | 4 | # Application version 5 | APP_VERSION='1.0.1' 6 | 7 | # Language 8 | APP_COUNTRY_NAME='Brazil' 9 | APP_COUNTRY_CODE='br' 10 | 11 | # Folder used to save the files 12 | PUBLIC_DIR=./public 13 | 14 | # Sync interval(show in page) 15 | SLEEP=300 16 | 17 | # Maintainer information 18 | MAINTAINER_MIRROR='Webysther' 19 | MAINTAINER_PROFILE='https://github.com/Webysther' 20 | MAINTAINER_REPO='https://github.com/Webysther/packagist-mirror' 21 | MAINTAINER_LICENSE='MIT License' 22 | 23 | # Main mirror used to get providers 24 | MAIN_MIRROR=https://packagist.org 25 | 26 | # Timezone 27 | TZ='America/Sao_Paulo' 28 | 29 | # Data mirror used to download repositories data 30 | # Japan - https://packagist.jp 31 | # Indonesia - https://packagist.phpindonesia.id 32 | # Brazil - https://packagist.com.br 33 | # China - https://packagist.phpcomposer.com (not fully compatible - too much broken packages) 34 | DATA_MIRROR=https://packagist.jp,https://packagist.com.br,https://packagist.phpindonesia.id 35 | 36 | # Max connections by mirror 37 | MAX_CONNECTIONS=25 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Webysther Nunes 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 furnished 10 | to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webysther/packagist-mirror", 3 | "description": "Build mirror of packagist", 4 | "keywords": ["packagist", "mirror"], 5 | "license": "CC0-1.0", 6 | "authors": [ 7 | { "name": "Webysther", "email": "websther@gmail.com" } 8 | ], 9 | "require": { 10 | "php": ">=7.1", 11 | "guzzlehttp/guzzle": "~6", 12 | "vlucas/phpdotenv": "~2", 13 | "symfony/console": "~3", 14 | "sebastian/version": "~2", 15 | "nesbot/carbon": "~1", 16 | "dariuszp/cli-progress-bar": "~1", 17 | "php-snippets/circular-array": "~1", 18 | "league/flysystem-cached-adapter": "~1", 19 | "illuminate/support": "~5" 20 | }, 21 | "require-dev": { 22 | "webysther/composer-plugin-qa": "~1", 23 | "webysther/composer-meta-qa": "~1", 24 | "league/flysystem-memory": "~1", 25 | "mikey179/vfsStream": "~1" 26 | }, 27 | "suggest": { 28 | "hirak/prestissimo": "Composer parallel install plugin" 29 | }, 30 | "autoload": { 31 | "psr-4": { "Webs\\Mirror\\": "src/" } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { "Webs\\Mirror\\Tests\\": "tests/" } 35 | }, 36 | "bin": [ 37 | "bin/mirror" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # PHP CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-php/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: webysther/composer-debian 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mysql:9.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "composer.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: composer install -n --prefer-dist 30 | 31 | - save_cache: 32 | paths: 33 | - ./vendor 34 | key: v1-dependencies-{{ checksum "composer.json" }} 35 | 36 | - run: mkdir -p $CIRCLE_TEST_REPORTS/phpunit 37 | - run: vendor/bin/phpunit --log-junit $CIRCLE_TEST_REPORTS/phpunit/junit.xml 38 | - run: vendor/bin/phpunit --coverage-clover=coverage.xml 39 | - run: bash <(curl -s https://codecov.io/bash) 40 | -------------------------------------------------------------------------------- /src/IProgressBar.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | interface IProgressBar 23 | { 24 | /** 25 | * Set console I/O. 26 | * 27 | * @param InputInterface $input Input 28 | * @param OutputInterface $output Output 29 | */ 30 | public function setConsole(InputInterface $input, OutputInterface $output):void; 31 | 32 | /** 33 | * Check if progress bar is enabled. 34 | * 35 | * @return bool True if enabled 36 | */ 37 | public function isDisabled():bool; 38 | 39 | /** 40 | * Start progress bar. 41 | * 42 | * @param int $total Total 43 | */ 44 | public function start(int $total):IProgressBar; 45 | 46 | /** 47 | * Update progress bar to some point. 48 | * 49 | * @param int|int $current Current value to set 50 | */ 51 | public function progress(int $current = 0):IProgressBar; 52 | 53 | /** 54 | * Finish progress bar. 55 | */ 56 | public function end():IProgressBar; 57 | } 58 | -------------------------------------------------------------------------------- /src/GZip.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | trait GZip 20 | { 21 | /** 22 | * Check if is gzip, if not compress. 23 | * 24 | * @param string $gzip 25 | * 26 | * @return string 27 | */ 28 | public function encode(string $gzip):string 29 | { 30 | if ($this->isGzip($gzip)) { 31 | return $gzip; 32 | } 33 | 34 | return gzencode($gzip); 35 | } 36 | 37 | /** 38 | * Check if is gzip, if yes uncompress. 39 | * 40 | * @param string $gzip 41 | * 42 | * @return string 43 | */ 44 | public function decode(string $gzip):string 45 | { 46 | if ($this->isGzip($gzip)) { 47 | return gzdecode($gzip); 48 | } 49 | 50 | return $gzip; 51 | } 52 | 53 | /** 54 | * Check if is gzip. 55 | * 56 | * @param string $gzip 57 | * 58 | * @return bool 59 | */ 60 | public function isGzip(string $gzip):bool 61 | { 62 | if (mb_strpos($gzip, "\x1f"."\x8b"."\x08") === 0) { 63 | return true; 64 | } 65 | 66 | return false; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/IO.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | trait IO 22 | { 23 | /** 24 | * @var string 25 | */ 26 | protected $directory; 27 | 28 | /** 29 | * Glob without file sort. 30 | * 31 | * @param string $pattern 32 | * 33 | * @return array 34 | */ 35 | public function glob(string $pattern):array 36 | { 37 | $return = glob($this->getFullPath($pattern), GLOB_NOSORT); 38 | 39 | if ($return === false) { 40 | return []; 41 | } 42 | 43 | return $return; 44 | } 45 | 46 | /** 47 | * Count files inside folder, if is a file, return 0. 48 | * 49 | * @param string $folder 50 | * 51 | * @return int 52 | */ 53 | public function getCount(string $folder):int 54 | { 55 | $path = $this->getFullPath($folder); 56 | 57 | if (!is_dir($path)) { 58 | $path = dirname($path); 59 | } 60 | 61 | $iterator = new FilesystemIterator( 62 | $path, 63 | FilesystemIterator::SKIP_DOTS 64 | ); 65 | 66 | return iterator_count($iterator); 67 | } 68 | 69 | /** 70 | * Get full path. 71 | * 72 | * @param string $path 73 | * 74 | * @return string 75 | */ 76 | public function getFullPath(string $path):string 77 | { 78 | if (strpos($path, $this->directory) !== false) { 79 | return $path; 80 | } 81 | 82 | return $this->directory.$path; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /bin/mirror: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | load(); 36 | 37 | date_default_timezone_set(getenv('TZ')); 38 | 39 | // Using Sebastian version formatter 40 | $version = new SebastianBergmann\Version( 41 | getenv('APP_VERSION'), getcwd() 42 | ); 43 | 44 | $cli = new Symfony\Component\Console\Application( 45 | getenv('APP_NAME'), 46 | $version->getVersion() 47 | ); 48 | 49 | $filesystem = new Webs\Mirror\Filesystem(getenv('PUBLIC_DIR')); 50 | $provider = new Webs\Mirror\Provider; 51 | $package = new Webs\Mirror\Package; 52 | $progressBar = new Webs\Mirror\ProgressBar; 53 | 54 | $mirror = new Webs\Mirror\Mirror( 55 | getenv('MAIN_MIRROR'), 56 | explode(',', getenv('DATA_MIRROR')) 57 | ); 58 | $http = new Webs\Mirror\Http($mirror, (int) getenv('MAX_CONNECTIONS')); 59 | 60 | $clean = new Webs\Mirror\Command\Clean(); 61 | $clean->setProgressBar($progressBar); 62 | $clean->setProvider($provider); 63 | $clean->setPackage($package); 64 | $clean->setFilesystem($filesystem); 65 | $clean->setHttp($http); 66 | 67 | $create = new Webs\Mirror\Command\Create(); 68 | $create->setProgressBar($progressBar); 69 | $create->setProvider($provider); 70 | $create->setPackage($package); 71 | $create->setFilesystem($filesystem); 72 | $create->setClean($clean); 73 | $create->setHttp($http); 74 | 75 | $cli->add($create); 76 | $cli->add($clean); 77 | $cli->run(); 78 | -------------------------------------------------------------------------------- /src/Mirror.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class Mirror 22 | { 23 | /** 24 | * @var string 25 | */ 26 | protected $master; 27 | 28 | /** 29 | * @var array 30 | */ 31 | protected $slaves; 32 | 33 | /** 34 | * @var CircularArray 35 | */ 36 | protected $all; 37 | 38 | /** 39 | * @var array 40 | */ 41 | protected $data; 42 | 43 | /** 44 | * @param string $master 45 | * @param array $slaves 46 | */ 47 | public function __construct(string $master, array $slaves) 48 | { 49 | $this->master = $master; 50 | $this->slaves = $slaves; 51 | 52 | $this->data = $slaves; 53 | if (!in_array($master, $this->data)) { 54 | $this->data[] = $master; 55 | } 56 | 57 | $this->all = CircularArray::fromArray($this->data); 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getMaster():string 64 | { 65 | return $this->master; 66 | } 67 | 68 | /** 69 | * Get all mirrors. 70 | * 71 | * @return CircularArray 72 | */ 73 | public function getAll():CircularArray 74 | { 75 | return $this->all; 76 | } 77 | 78 | /** 79 | * Get next item. 80 | * 81 | * @return string 82 | */ 83 | public function getNext():string 84 | { 85 | $this->all->next(); 86 | 87 | return $this->all->current(); 88 | } 89 | 90 | /** 91 | * @param string $value 92 | * 93 | * @return CircularArray 94 | */ 95 | public function remove(string $value):CircularArray 96 | { 97 | $this->data = array_values(array_diff($this->data, [$value])); 98 | $this->all = CircularArray::fromArray($this->data); 99 | 100 | return $this->getAll(); 101 | } 102 | 103 | /** 104 | * @return array 105 | */ 106 | public function toArray():array 107 | { 108 | return $this->getAll()->toArray(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/ProgressBar.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class ProgressBar implements IProgressBar 22 | { 23 | use Console; 24 | 25 | /** 26 | * @var bool 27 | */ 28 | protected $disabled; 29 | 30 | /** 31 | * @var CliProgressBar 32 | */ 33 | protected $progressBar; 34 | 35 | /**s 36 | * @var int 37 | */ 38 | protected $total; 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function isDisabled():bool 44 | { 45 | if (isset($this->disabled)) { 46 | return $this->disabled; 47 | } 48 | 49 | $isQuiet = $this->output->isQuiet(); 50 | $noProgress = $this->input->getOption('no-progress'); 51 | $noAnsi = $this->input->getOption('no-ansi'); 52 | 53 | $this->disabled = $isQuiet || $noProgress || $noAnsi; 54 | 55 | return $this->disabled; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function start(int $total):IProgressBar 62 | { 63 | if ($this->isDisabled()) { 64 | return $this; 65 | } 66 | 67 | $this->total = $total; 68 | $this->progressBar = new CliProgressBar($total, 0); 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function progress(int $current = 0):IProgressBar 77 | { 78 | if ($this->isDisabled()) { 79 | return $this; 80 | } 81 | 82 | if ($current) { 83 | $this->progressBar->progress($current); 84 | 85 | return $this; 86 | } 87 | 88 | $this->progressBar->progress(); 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | public function end():IProgressBar 97 | { 98 | if ($this->isDisabled()) { 99 | return $this; 100 | } 101 | 102 | $this->progressBar->progress($this->total); 103 | $this->progressBar->end(); 104 | 105 | return $this; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Package.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class Package 24 | { 25 | use Console; 26 | 27 | /** 28 | * @var array 29 | */ 30 | protected $packagesDownloaded = []; 31 | 32 | /** 33 | * @var Http 34 | */ 35 | protected $http; 36 | 37 | /** 38 | * @var Filesystem 39 | */ 40 | protected $filesystem; 41 | 42 | /** 43 | * @var stdClass 44 | */ 45 | protected $mainJson; 46 | 47 | /** 48 | * Main files. 49 | */ 50 | const MAIN = Base::MAIN; 51 | 52 | /** 53 | * Add a http. 54 | * 55 | * @param Http $http 56 | * 57 | * @return Package 58 | */ 59 | public function setHttp(Http $http):Package 60 | { 61 | $this->http = $http; 62 | 63 | return $this; 64 | } 65 | 66 | /** 67 | * Add a fileSystem. 68 | * 69 | * @param Filesystem $fileSystem 70 | * 71 | * @return Package 72 | */ 73 | public function setFilesystem(Filesystem $filesystem):Package 74 | { 75 | $this->filesystem = $filesystem; 76 | 77 | return $this; 78 | } 79 | 80 | /** 81 | * @param string $path 82 | */ 83 | public function setDownloaded(string $path):Package 84 | { 85 | $this->packagesDownloaded[] = $path; 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * @return array 92 | */ 93 | public function getDownloaded():array 94 | { 95 | return $this->packagesDownloaded; 96 | } 97 | 98 | /** 99 | * @return stdClass 100 | */ 101 | public function getMainJson():stdClass 102 | { 103 | if (isset($this->mainJson)) { 104 | return $this->mainJson; 105 | } 106 | 107 | $this->mainJson = $this->http->getJson(self::MAIN); 108 | 109 | return $this->mainJson; 110 | } 111 | 112 | /** 113 | * @param stdClass $obj 114 | * @return Package 115 | */ 116 | public function setMainJson(stdClass $obj):Package 117 | { 118 | $this->mainJson = $obj; 119 | 120 | return $this; 121 | } 122 | 123 | /** 124 | * @param stdClass $providers 125 | * 126 | * @return array 127 | */ 128 | public function normalize(stdClass $providers):array 129 | { 130 | $providerPackages = []; 131 | foreach ($providers as $name => $hash) { 132 | $uri = sprintf('p/%s$%s.json', $name, $hash->sha256); 133 | $providerPackages[$uri] = $hash->sha256; 134 | } 135 | 136 | return $providerPackages; 137 | } 138 | 139 | /** 140 | * @param string $uri 141 | * 142 | * @return array 143 | */ 144 | public function getProvider(string $uri):array 145 | { 146 | $providers = json_decode($this->filesystem->read($uri))->providers; 147 | 148 | return $this->normalize($providers); 149 | } 150 | 151 | /** 152 | * Download only a package. 153 | * 154 | * @param array $providerPackages Provider Packages 155 | * 156 | * @return Generator Providers downloaded 157 | */ 158 | public function getGenerator(array $providerPackages):Generator 159 | { 160 | $providerPackages = array_keys($providerPackages); 161 | foreach ($providerPackages as $uri) { 162 | if ($this->filesystem->has($uri)) { 163 | continue; 164 | } 165 | 166 | yield $uri => $this->http->getRequest($uri); 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Provider.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class Provider 24 | { 25 | use Console; 26 | 27 | /** 28 | * @var Http 29 | */ 30 | protected $http; 31 | 32 | /** 33 | * @var Filesystem 34 | */ 35 | protected $filesystem; 36 | 37 | /** 38 | * @var array 39 | */ 40 | protected $providersDownloaded = []; 41 | 42 | /** 43 | * @var bool 44 | */ 45 | protected $initialized = false; 46 | 47 | /** 48 | * Add a http. 49 | * 50 | * @param Http $http 51 | * 52 | * @return Provider 53 | */ 54 | public function setHttp(Http $http):Provider 55 | { 56 | $this->http = $http; 57 | 58 | return $this; 59 | } 60 | 61 | /** 62 | * Add a fileSystem. 63 | * 64 | * @param Filesystem $fileSystem 65 | * 66 | * @return Provider 67 | */ 68 | public function setFilesystem(Filesystem $filesystem):Provider 69 | { 70 | $this->filesystem = $filesystem; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * @param string $path 77 | */ 78 | public function setDownloaded(string $path):Provider 79 | { 80 | $this->providersDownloaded[] = $path; 81 | 82 | return $this; 83 | } 84 | 85 | /** 86 | * @return array 87 | */ 88 | public function getDownloaded():array 89 | { 90 | return $this->providersDownloaded; 91 | } 92 | 93 | /** 94 | * @param bool $value 95 | */ 96 | public function setInitialized(bool $value):Provider 97 | { 98 | $this->initialized = $value; 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Add base url of packagist.org to services on packages.json of 105 | * mirror don't support. 106 | * 107 | * @param stdClass $providers List of providers from packages.json 108 | */ 109 | public function addFullPath(stdClass $providers):stdClass 110 | { 111 | // Add full path for services of mirror don't provide only packagist.org 112 | foreach (['notify', 'notify-batch', 'search'] as $key) { 113 | // Just in case packagist.org add full path in future 114 | $path = parse_url($providers->$key){'path'}; 115 | $providers->$key = $this->http->getBaseUri().$path; 116 | } 117 | 118 | return $providers; 119 | } 120 | 121 | /** 122 | * Load provider includes. 123 | * 124 | * @param stdClass $providers 125 | * 126 | * @return array 127 | */ 128 | public function normalize(stdClass $providers):array 129 | { 130 | if (!property_exists($providers, 'provider-includes')) { 131 | throw new Exception('Not found providers information'); 132 | } 133 | 134 | $providerIncludes = $providers->{'provider-includes'}; 135 | 136 | $includes = []; 137 | foreach ($providerIncludes as $name => $hash) { 138 | $uri = str_replace('%hash%', $hash->sha256, $name); 139 | $includes[$uri] = $hash->sha256; 140 | } 141 | 142 | return $includes; 143 | } 144 | 145 | /** 146 | * Download packages.json & provider-xxx$xxx.json. 147 | * 148 | * @param array $providerIncludes Provider Includes 149 | * 150 | * @return Generator Providers downloaded 151 | */ 152 | public function getGenerator(array $providerIncludes):Generator 153 | { 154 | $providerIncludes = array_keys($providerIncludes); 155 | $updated = true; 156 | foreach ($providerIncludes as $uri) { 157 | if ($this->filesystem->has($uri) && !$this->initialized) { 158 | continue; 159 | } 160 | 161 | $updated = false; 162 | yield $uri => $this->http->getRequest($uri); 163 | } 164 | 165 | return $updated; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/Command/Base.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class Base extends Command 31 | { 32 | use ShortName; 33 | 34 | /** 35 | * @var bool 36 | */ 37 | protected $initialized = false; 38 | 39 | /** 40 | * @var InputInterface 41 | */ 42 | protected $input; 43 | 44 | /** 45 | * @var OutputInterface 46 | */ 47 | protected $output; 48 | 49 | /** 50 | * @var IProgressBar 51 | */ 52 | protected $progressBar; 53 | 54 | /** 55 | * @var Filesystem 56 | */ 57 | protected $filesystem; 58 | 59 | /** 60 | * @var Http 61 | */ 62 | protected $http; 63 | 64 | /** 65 | * @var Provider 66 | */ 67 | protected $provider; 68 | 69 | /** 70 | * @var Package 71 | */ 72 | protected $package; 73 | 74 | /** 75 | * @var int 76 | */ 77 | protected $exitCode; 78 | 79 | /** 80 | * @var bool 81 | */ 82 | protected $verboseVerbose = false; 83 | 84 | /** 85 | * @var int 86 | */ 87 | const VV = OutputInterface::VERBOSITY_VERBOSE; 88 | 89 | /** 90 | * Main files. 91 | */ 92 | const MAIN = 'packages.json'; 93 | const DOT = '.packages.json'; 94 | const INIT = '.init'; 95 | const TO = 'p'; 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | protected function configure() 101 | { 102 | $this->addOption( 103 | 'no-progress', 104 | null, 105 | InputOption::VALUE_NONE, 106 | "Don't show progress bar" 107 | ); 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | protected function initialize(InputInterface $input, OutputInterface $output) 114 | { 115 | $this->input = $input; 116 | $this->output = $output; 117 | $this->verboseVerbose = $this->output->getVerbosity() >= self::VV; 118 | } 119 | 120 | /** 121 | * Inicialize with custom 122 | * 123 | * @param InputInterface $input 124 | * @param OutputInterface $output 125 | */ 126 | public function init(InputInterface $input, OutputInterface $output) 127 | { 128 | if (isset($this->input) && isset($this->output)) { 129 | return $this; 130 | } 131 | 132 | // Only when direct call by tests 133 | $this->initialize($input, $output); 134 | 135 | return $this; 136 | } 137 | 138 | /** 139 | * @return bool 140 | */ 141 | public function isVerbose():bool 142 | { 143 | return $this->verboseVerbose; 144 | } 145 | 146 | /** 147 | * Add a progress bar. 148 | * 149 | * @param IProgressBar $progressBar 150 | * 151 | * @return Base 152 | */ 153 | public function setProgressBar(IProgressBar $progressBar):Base 154 | { 155 | $this->progressBar = $progressBar; 156 | 157 | return $this; 158 | } 159 | 160 | /** 161 | * Add a fileSystem. 162 | * 163 | * @param Filesystem $fileSystem 164 | * 165 | * @return Base 166 | */ 167 | public function setFilesystem(Filesystem $filesystem):Base 168 | { 169 | $this->filesystem = $filesystem; 170 | 171 | return $this; 172 | } 173 | 174 | /** 175 | * Add a http. 176 | * 177 | * @param Http $http 178 | * 179 | * @return Base 180 | */ 181 | public function setHttp(Http $http):Base 182 | { 183 | $this->http = $http; 184 | 185 | return $this; 186 | } 187 | 188 | /** 189 | * Add a provider. 190 | * 191 | * @param Provider $provider 192 | * 193 | * @return Base 194 | */ 195 | public function setProvider(Provider $provider):Base 196 | { 197 | $this->provider = $provider; 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * Add a packages. 204 | * 205 | * @param Package $package 206 | * 207 | * @return Base 208 | */ 209 | public function setPackage(Package $package):Base 210 | { 211 | $this->package = $package; 212 | 213 | return $this; 214 | } 215 | 216 | /** 217 | * @param int $exit 218 | * 219 | * @return Base 220 | */ 221 | protected function setExitCode(int $exit):Base 222 | { 223 | $this->exitCode = $exit; 224 | 225 | return $this; 226 | } 227 | 228 | /** 229 | * @return int 230 | */ 231 | protected function getExitCode():int 232 | { 233 | return $this->stop() ? $this->exitCode : 0; 234 | } 235 | 236 | /** 237 | * @return bool 238 | */ 239 | protected function stop():bool 240 | { 241 | return isset($this->exitCode); 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/Http.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class Http 28 | { 29 | use Gzip; 30 | 31 | /** 32 | * @var Client 33 | */ 34 | protected $client; 35 | 36 | /** 37 | * @var Mirror 38 | */ 39 | protected $mirror; 40 | 41 | /** 42 | * @var array 43 | */ 44 | protected $poolErrors; 45 | 46 | /** 47 | * @var array 48 | */ 49 | protected $poolErrorsCount = []; 50 | 51 | /** 52 | * @var array 53 | */ 54 | protected $config = [ 55 | 'base_uri' => '', 56 | 'headers' => ['Accept-Encoding' => 'gzip'], 57 | 'decode_content' => false, 58 | 'timeout' => 30, 59 | 'connect_timeout' => 15, 60 | ]; 61 | 62 | /** 63 | * @var int 64 | */ 65 | protected $maxConnections; 66 | 67 | /** 68 | * @var int 69 | */ 70 | protected $connections; 71 | 72 | /** 73 | * @var bool 74 | */ 75 | protected $usingMirrors = false; 76 | 77 | /** 78 | * @param Mirror $mirror 79 | * @param int $maxConnections 80 | */ 81 | public function __construct(Mirror $mirror, int $maxConnections) 82 | { 83 | $this->config['base_uri'] = $mirror->getMaster(); 84 | $this->client = new Client($this->config); 85 | $this->maxConnections = $maxConnections; 86 | $this->mirror = $mirror; 87 | } 88 | 89 | /** 90 | * @return string 91 | */ 92 | public function getBaseUri():string 93 | { 94 | return $this->config['base_uri']; 95 | } 96 | 97 | /** 98 | * Client get with transparent gz decode. 99 | * 100 | * @see Client::get 101 | */ 102 | public function getJson(string $uri):stdClass 103 | { 104 | $response = $this->client->get($uri); 105 | 106 | // Maybe 4xx or 5xx 107 | if ($response->getStatusCode() >= 400) { 108 | throw new Exception("Error download $uri", 1); 109 | } 110 | 111 | $json = $this->decode((string) $response->getBody()); 112 | $decoded = json_decode($json); 113 | 114 | if (json_last_error() !== JSON_ERROR_NONE) { 115 | throw new Exception("Response not a json: $json", 1); 116 | } 117 | 118 | return $decoded; 119 | } 120 | 121 | /** 122 | * Create a new get request. 123 | * 124 | * @param string $uri 125 | * 126 | * @return Request 127 | */ 128 | public function getRequest(string $uri):Request 129 | { 130 | $base = $this->getBaseUri(); 131 | if ($this->usingMirrors) { 132 | $base = $this->mirror->getNext(); 133 | } 134 | 135 | return new Request('GET', $base.'/'.$uri); 136 | } 137 | 138 | /** 139 | * @param Generator $requests 140 | * @param Closure $success 141 | * @param Closure $complete 142 | * 143 | * @return Http 144 | */ 145 | public function pool(Generator $requests, Closure $success, Closure $complete):Http 146 | { 147 | $this->connections = $this->maxConnections; 148 | if ($this->usingMirrors) { 149 | $mirrors = $this->mirror->getAll()->count(); 150 | $this->connections = $this->maxConnections * $mirrors; 151 | } 152 | 153 | $fulfilled = function ($response, $path) use ($success, $complete) { 154 | $body = (string) $response->getBody(); 155 | $success($body, $path); 156 | $complete(); 157 | }; 158 | 159 | $rejected = function ($reason, $path) use ($complete) { 160 | $uri = $reason->getRequest()->getUri(); 161 | $host = $uri->getScheme().'://'.$uri->getHost(); 162 | 163 | $wordwrap = wordwrap($reason->getMessage()); 164 | $message = current(explode("\n", $wordwrap)).'...'; 165 | 166 | $this->poolErrors[$path] = [ 167 | 'code' => $reason->getCode(), 168 | 'host' => $host, 169 | 'message' => $message, 170 | ]; 171 | 172 | if (!isset($this->poolErrorsCount[$host])) { 173 | $this->poolErrorsCount[$host] = 0; 174 | } 175 | ++$this->poolErrorsCount[$host]; 176 | $complete(); 177 | }; 178 | 179 | $this->poolErrors = []; 180 | $pool = new Pool( 181 | $this->client, 182 | $requests, 183 | [ 184 | 'concurrency' => $this->connections, 185 | 'fulfilled' => $fulfilled, 186 | 'rejected' => $rejected, 187 | ] 188 | ); 189 | $pool->promise()->wait(); 190 | 191 | // Reset to use only max connections for one mirror 192 | $this->usingMirrors = false; 193 | 194 | return $this; 195 | } 196 | 197 | /** 198 | * @return Http 199 | */ 200 | public function useMirrors():Http 201 | { 202 | $this->usingMirrors = true; 203 | 204 | return $this; 205 | } 206 | 207 | /** 208 | * @return array 209 | */ 210 | public function getPoolErrors():array 211 | { 212 | return $this->poolErrors; 213 | } 214 | 215 | /** 216 | * @param string $mirror 217 | * 218 | * @return int 219 | */ 220 | public function getTotalErrorByMirror(string $mirror):int 221 | { 222 | if (!isset($this->poolErrorsCount[$mirror])) { 223 | return 0; 224 | } 225 | 226 | return $this->poolErrorsCount[$mirror]; 227 | } 228 | 229 | /** 230 | * @return Mirror 231 | */ 232 | public function getMirror():Mirror 233 | { 234 | return $this->mirror; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Command/Clean.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class Clean extends Base 25 | { 26 | /** 27 | * @var array 28 | */ 29 | protected $changed = []; 30 | 31 | /** 32 | * @var array 33 | */ 34 | protected $removed = []; 35 | 36 | /** 37 | * @var bool 38 | */ 39 | protected $isScrub = false; 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function __construct($name = '') 45 | { 46 | parent::__construct('clean'); 47 | $this->setDescription( 48 | 'Clean outdated files of mirror' 49 | ); 50 | } 51 | 52 | /** 53 | * Console params configuration. 54 | */ 55 | protected function configure():void 56 | { 57 | parent::configure(); 58 | $this->addOption( 59 | 'scrub', 60 | null, 61 | InputOption::VALUE_NONE, 62 | 'Check all directories for old files, use only to check all disk' 63 | ); 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function execute(InputInterface $input, OutputInterface $output):int 70 | { 71 | $this->initialize($input, $output); 72 | $this->bootstrap(); 73 | 74 | if ($input->hasOption('scrub') && $input->getOption('scrub')) { 75 | $this->isScrub = true; 76 | } 77 | 78 | $this->flushProviders(); 79 | $this->flushPackages(); 80 | 81 | if (!count($this->changed)) { 82 | $output->writeln('Nothing to clean'); 83 | } 84 | 85 | return $this->getExitCode(); 86 | } 87 | 88 | /** 89 | * @return void 90 | */ 91 | public function bootstrap():void 92 | { 93 | $this->progressBar->setConsole($this->input, $this->output); 94 | $this->package->setConsole($this->input, $this->output); 95 | $this->package->setHttp($this->http); 96 | $this->package->setFilesystem($this->filesystem); 97 | $this->provider->setConsole($this->input, $this->output); 98 | $this->provider->setHttp($this->http); 99 | $this->provider->setFilesystem($this->filesystem); 100 | } 101 | 102 | /** 103 | * Flush old cached files of providers. 104 | * 105 | * @return Clean 106 | */ 107 | protected function flushProviders():Clean 108 | { 109 | if (!$this->filesystem->hasFile(self::MAIN)) { 110 | return $this; 111 | } 112 | 113 | $providers = json_decode($this->filesystem->read(self::MAIN)); 114 | $includes = array_keys($this->provider->normalize($providers)); 115 | 116 | $this->initialized = $this->filesystem->hasFile(self::INIT); 117 | 118 | foreach ($includes as $uri) { 119 | $pattern = $this->filesystem->getGzName($this->shortname($uri)); 120 | $glob = $this->filesystem->glob($pattern); 121 | 122 | $this->output->writeln( 123 | 'Check old file of '. 124 | $pattern. 125 | '' 126 | ); 127 | 128 | // If not have one file or not scrumbbing 129 | if (!(count($glob) > 1 || $this->isScrub)) { 130 | continue; 131 | } 132 | 133 | $this->changed[] = $uri; 134 | $uri = $this->filesystem->getFullPath($this->filesystem->getGzName($uri)); 135 | $diff = array_diff($glob, [$uri]); 136 | $this->removeAll($diff)->showRemoved(); 137 | } 138 | 139 | return $this; 140 | } 141 | 142 | /** 143 | * Flush old cached files of packages. 144 | * 145 | * @return bool True if work, false otherside 146 | */ 147 | protected function flushPackages():bool 148 | { 149 | $increment = 0; 150 | 151 | foreach ($this->changed as $uri) { 152 | $providers = json_decode($this->filesystem->read($uri)); 153 | $list = $this->package->normalize($providers->providers); 154 | 155 | $this->output->writeln( 156 | '['.++$increment.'/'.count($this->changed).'] '. 157 | 'Check old packages for provider '. 158 | ''.$this->shortname($uri).'' 159 | ); 160 | $this->progressBar->start(count($list)); 161 | $this->flushPackage(array_keys($list)); 162 | $this->progressBar->end(); 163 | $this->showRemoved(); 164 | } 165 | 166 | return true; 167 | } 168 | 169 | /** 170 | * Flush from one provider. 171 | * 172 | * @param array $list List of packages 173 | */ 174 | protected function flushPackage(array $list):void 175 | { 176 | $packages = $this->package->getDownloaded(); 177 | 178 | foreach ($list as $uri) { 179 | $this->progressBar->progress(); 180 | 181 | if ($this->canSkipPackage($uri, $packages)) { 182 | continue; 183 | } 184 | 185 | $gzName = $this->filesystem->getGzName($uri); 186 | $pattern = $this->shortname($gzName); 187 | $glob = $this->filesystem->glob($pattern); 188 | 189 | // If only have the file dont exist old files 190 | if (count($glob) < 2) { 191 | continue; 192 | } 193 | 194 | // Remove current value 195 | $fullPath = $this->filesystem->getFullPath($gzName); 196 | $diff = array_diff($glob, [$fullPath]); 197 | $this->removeAll($diff); 198 | } 199 | } 200 | 201 | /** 202 | * @param string $uri 203 | * @param array $packages 204 | * @return bool 205 | */ 206 | protected function canSkipPackage(string $uri, array $packages):bool 207 | { 208 | if ($this->initialized) { 209 | return true; 210 | } 211 | 212 | $folder = dirname($uri); 213 | 214 | // This uri was changed by last download? 215 | if (count($packages) && !in_array($uri, $packages)) { 216 | return true; 217 | } 218 | 219 | // If only have the file and link dont exist old files 220 | if ($this->filesystem->getCount($folder) < 3) { 221 | return true; 222 | } 223 | 224 | return false; 225 | } 226 | 227 | /** 228 | * Remove all files 229 | * 230 | * @param array $files 231 | * @return Clean 232 | */ 233 | protected function removeAll(array $files):Clean 234 | { 235 | foreach ($files as $file) { 236 | $this->filesystem->delete($file); 237 | } 238 | 239 | $this->removed = []; 240 | if ($this->isVerbose()) { 241 | $this->removed = $files; 242 | } 243 | 244 | return $this; 245 | } 246 | 247 | /** 248 | * Show packages removed. 249 | * 250 | * @return Clean 251 | */ 252 | protected function showRemoved():Clean 253 | { 254 | $base = getenv('PUBLIC_DIR').DIRECTORY_SEPARATOR; 255 | 256 | foreach ($this->removed as $file) { 257 | $file = str_replace($base, '', $file); 258 | $this->output->writeln( 259 | 'File '.$file.' was removed!' 260 | ); 261 | } 262 | 263 | $this->removed = []; 264 | 265 | return $this; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/Filesystem.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class Filesystem 24 | { 25 | use GZip; 26 | use IO; 27 | 28 | /** 29 | * @var FlyFilesystem 30 | */ 31 | protected $filesystem; 32 | 33 | /** 34 | * Ephemeral cache for folder files count. 35 | * 36 | * @var array 37 | */ 38 | protected $countedFolder = []; 39 | 40 | /** 41 | * @param string $dir Base directory 42 | * @param bool $initialize If true initialize the filesystem access 43 | */ 44 | public function __construct($baseDirectory) 45 | { 46 | $this->directory = $baseDirectory.'/'; 47 | 48 | // Create the adapter 49 | $localAdapter = new Local($this->directory); 50 | 51 | // And use that to create the file system 52 | $this->filesystem = new FlyFilesystem($localAdapter); 53 | } 54 | 55 | /** 56 | * @param FlyFilesystem $filesystem 57 | */ 58 | public function setFilesystem(FlyFilesystem $filesystem):Filesystem 59 | { 60 | $this->filesystem = $filesystem; 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Add suffix gz to json file. 67 | * 68 | * @param string $path 69 | * 70 | * @return string 71 | */ 72 | public function getGzName(string $path):string 73 | { 74 | $fullPath = $this->getFullPath($path); 75 | $extension = pathinfo($fullPath, PATHINFO_EXTENSION); 76 | 77 | if ($extension == 'json') { 78 | return $path.'.gz'; 79 | } 80 | 81 | return $path; 82 | } 83 | 84 | /** 85 | * Get link name from gz. 86 | * 87 | * @param string $path 88 | * 89 | * @return string 90 | */ 91 | protected function getLink(string $path):string 92 | { 93 | $fullPath = $this->getFullPath($path); 94 | $extension = pathinfo($fullPath, PATHINFO_EXTENSION); 95 | 96 | if ($extension == 'gz') { 97 | return substr($path, 0, -3); 98 | } 99 | 100 | return $path; 101 | } 102 | 103 | /** 104 | * Decode from gz after read from disk. 105 | * 106 | * @see FlyFilesystem::read 107 | */ 108 | public function read(string $path):string 109 | { 110 | $path = $this->getGzName($path); 111 | $file = $this->filesystem->read($path); 112 | 113 | if ($file === false) { 114 | return ''; 115 | } 116 | 117 | return (string) $this->decode($file); 118 | } 119 | 120 | /** 121 | * Encode to gz before write to disk with hash checking. 122 | * 123 | * @see FlyFilesystem::write 124 | */ 125 | public function write(string $path, string $contents):Filesystem 126 | { 127 | $file = $this->getGzName($path); 128 | $this->filesystem->put($file, $this->encode($contents)); 129 | $decoded = $this->decode($contents); 130 | 131 | if ($this->getHash($decoded) != $this->getHashFile($file)) { 132 | $this->filesystem->delete($file); 133 | throw new Exception("Write file $path hash failed"); 134 | } 135 | 136 | $this->symlink($file); 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * Simple touch. 143 | * 144 | * @param string $path 145 | * 146 | * @return Filesystem 147 | */ 148 | public function touch(string $path):Filesystem 149 | { 150 | if ($this->has($path)) { 151 | return $this; 152 | } 153 | 154 | touch($this->getFullPath($path)); 155 | 156 | return $this; 157 | } 158 | 159 | /** 160 | * @param string $file 161 | * 162 | * @return bool 163 | */ 164 | protected function isGzFile(string $file):bool 165 | { 166 | if (substr($this->getGzName($file), -3) == '.gz') { 167 | return true; 168 | } 169 | 170 | return false; 171 | } 172 | 173 | /** 174 | * Create a symlink. 175 | * 176 | * @param string $file 177 | * 178 | * @return Filesystem 179 | */ 180 | protected function symlink(string $file):Filesystem 181 | { 182 | if (!$this->hasFile($file) || !$this->isGzFile($file)) { 183 | return $this; 184 | } 185 | 186 | $path = $this->getGzName($file); 187 | $link = $this->getFullPath($this->getLink($path)); 188 | 189 | if ($this->hasLink($link)) { 190 | return $this; 191 | } 192 | 193 | if (strpos($link, 'vfs://') !== false) { 194 | return $this; 195 | } 196 | 197 | symlink(basename($path), $link); 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * @see FlyFilesystem::has 204 | */ 205 | public function has(string $path):bool 206 | { 207 | return $this->hasFile($path) && $this->hasLink($path); 208 | } 209 | 210 | /** 211 | * @see FlyFilesystem::has 212 | */ 213 | public function hasFile(string $path):bool 214 | { 215 | return file_exists($this->getFullPath($this->getGzName($path))); 216 | } 217 | 218 | /** 219 | * @see FlyFilesystem::has 220 | */ 221 | protected function hasLink(string $path):bool 222 | { 223 | return is_link($this->getFullPath($this->getLink($path))); 224 | } 225 | 226 | /** 227 | * Move to not dot name of file. 228 | * 229 | * @param string $from 230 | * 231 | * @return Filesystem 232 | */ 233 | public function move(string $from):Filesystem 234 | { 235 | if (!$this->has($from)) { 236 | return $this; 237 | } 238 | 239 | $file = $this->getGzName($from); 240 | $target = substr($file, 1); 241 | 242 | if ($this->has($target)) { 243 | $this->delete($target); 244 | } 245 | 246 | retry(8, function () use ($from, $target) { 247 | $this->filesystem->rename($this->getGzName($from), $target); 248 | }, 250); 249 | 250 | $this->symlink($target); 251 | // remove old symlink 252 | $this->delete($from); 253 | 254 | return $this; 255 | } 256 | 257 | /** 258 | * @see FlyFilesystem::delete 259 | * @see FlyFilesystem::deleteDir 260 | */ 261 | public function delete(string $fileOrDirectory):Filesystem 262 | { 263 | $path = $this->getFullPath($fileOrDirectory); 264 | 265 | if (is_dir($path)) { 266 | $this->filesystem->deleteDir($fileOrDirectory); 267 | 268 | return $this; 269 | } 270 | 271 | $file = $this->getGzName($path); 272 | if (file_exists($file)) { 273 | unlink($file); 274 | } 275 | 276 | $link = $this->getLink($path); 277 | if (is_link($link)) { 278 | unlink($link); 279 | } 280 | 281 | return $this; 282 | } 283 | 284 | /** 285 | * Calculates SHA256. 286 | * 287 | * @param string $string 288 | * 289 | * @return string 290 | */ 291 | public function getHash(string $string):string 292 | { 293 | return hash('sha256', $string); 294 | } 295 | 296 | /** 297 | * Calculates SHA256 for file. 298 | * 299 | * @param string $path 300 | * 301 | * @return string 302 | */ 303 | public function getHashFile(string $path):string 304 | { 305 | // dont use hash_file because content is saved with gz 306 | return $this->getHash($this->read($path)); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /resources/index.html.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Packagist Mirror 7 | 8 | 9 | 10 | 11 | 12 | 39 | 40 | 41 |
42 |
43 |
44 |
45 |

46 | Packagist Mirror 47 | <?= $countryName; ?> 52 |

53 |


(Synchronized every seconds)

54 |
55 |

56 | This is PHP package repository Packagist.org mirror site. 57 |

58 |

59 | If you're using PHP Composer, commands like create-project, require, update, remove are often used. 60 | When those commands are executed, Composer will download information from the packages that are needed also from dependent packages. The number of json files downloaded depends on the complexity of the packages which are going to be used. 61 | The further you are from the location of the packagist.org server, the more time is needed to download json files. By using mirror, it will help save the time for downloading because the server location is closer. 62 |

63 |

64 | Please do the following command to change the PHP Composer config to use this site as default Composer repository. 65 |

66 |
67 | 68 | 69 |
70 |

71 | $ 72 | 73 |

74 |
75 | 76 | 77 |
78 |

79 | $ 80 | 81 |

82 |
83 |
84 | 85 |

Disclaimer

86 |

This site offers its services free of charge and only as a mirror site.

87 |

This site only provides package information / metadata with no distribution file of the packages. All packages metadata files are mirrored from Packagist.org. We do not modify and/or process the JSON files. If there is something wrong, please disable the setting the Disable command above and try to refer to the original packagist.org.

88 |
89 |
90 |
91 |
92 |
93 |

94 | Packagist Mirror was built from by 95 | . 96 |

97 |

98 | It is licensed under the . 99 | You can view the project's source code on GitHub. 100 |

101 |
102 |
103 | 104 | 105 | 106 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /src/Command/Create.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | class Create extends Base 28 | { 29 | /** 30 | * @var stdClass 31 | */ 32 | protected $providers; 33 | 34 | /** 35 | * @var array 36 | */ 37 | protected $providerIncludes; 38 | 39 | /** 40 | * @var string 41 | */ 42 | protected $currentProvider; 43 | 44 | /** 45 | * @var array 46 | */ 47 | protected $providerPackages; 48 | 49 | /** 50 | * @var Clean 51 | */ 52 | protected $clean; 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function __construct($name = '') 58 | { 59 | parent::__construct('create'); 60 | $this->setDescription( 61 | 'Create/update packagist mirror' 62 | ); 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function execute(InputInterface $input, OutputInterface $output):int 69 | { 70 | $this->initialize($input, $output); 71 | $this->bootstrap(); 72 | 73 | // Download providers 74 | $this->downloadProviders(); 75 | 76 | // Download packages 77 | if ($this->stop() || $this->downloadPackages()->stop()) { 78 | return $this->getExitCode(); 79 | } 80 | 81 | // Move to new location 82 | $this->filesystem->move(self::DOT); 83 | 84 | // Clean 85 | $this->setExitCode($this->clean->execute($input, $output)); 86 | 87 | if ($this->initialized) { 88 | $this->filesystem->delete(self::INIT); 89 | } 90 | 91 | return $this->getExitCode(); 92 | } 93 | 94 | /** 95 | * @return void 96 | */ 97 | public function bootstrap():void 98 | { 99 | $this->progressBar->setConsole($this->input, $this->output); 100 | $this->package->setConsole($this->input, $this->output); 101 | $this->package->setHttp($this->http); 102 | $this->package->setFilesystem($this->filesystem); 103 | $this->provider->setConsole($this->input, $this->output); 104 | $this->provider->setHttp($this->http); 105 | $this->provider->setFilesystem($this->filesystem); 106 | } 107 | 108 | /** 109 | * @param Clean $clean 110 | */ 111 | public function setClean(Clean $clean):Create 112 | { 113 | $this->clean = $clean; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * @return int 120 | */ 121 | protected function getExitCode():int 122 | { 123 | $this->generateHtml(); 124 | 125 | return parent::getExitCode(); 126 | } 127 | 128 | /** 129 | * Check if packages.json was changed. 130 | * 131 | * @return bool 132 | */ 133 | protected function isEqual():bool 134 | { 135 | // if 'p/...' folder not found 136 | if (!is_dir($this->filesystem->getFullPath(self::TO))) { 137 | $this->filesystem->touch(self::INIT); 138 | $this->moveToPublic(); 139 | } 140 | 141 | $this->initialized = $this->filesystem->hasFile(self::INIT); 142 | 143 | $newPackages = json_encode($this->providers, JSON_PRETTY_PRINT); 144 | 145 | // No provider changed? Just relax... 146 | if ($this->filesystem->has(self::MAIN) && !$this->initialized) { 147 | $old = $this->filesystem->getHashFile(self::MAIN); 148 | $new = $this->filesystem->getHash($newPackages); 149 | 150 | if ($old == $new) { 151 | $this->output->writeln(self::MAIN.' updated'); 152 | $this->setExitCode(0); 153 | 154 | return true; 155 | } 156 | } 157 | 158 | if (!$this->filesystem->has(self::MAIN)) { 159 | $this->initialized = true; 160 | } 161 | 162 | $this->provider->setInitialized($this->initialized); 163 | $this->filesystem->write(self::DOT, $newPackages); 164 | 165 | return false; 166 | } 167 | 168 | /** 169 | * Copy all public resources to public 170 | * 171 | * @return void 172 | */ 173 | protected function moveToPublic():void 174 | { 175 | $from = getcwd().'/resources/public/'; 176 | foreach (new \DirectoryIterator($from) as $fileInfo) { 177 | if($fileInfo->isDot()) continue; 178 | $file = $fileInfo->getFilename(); 179 | $to = $this->filesystem->getFullPath($file); 180 | copy($from.$file, $to); 181 | } 182 | } 183 | 184 | /** 185 | * Download packages.json & provider-xxx$xxx.json. 186 | * 187 | * @return Create 188 | */ 189 | protected function downloadProviders():Create 190 | { 191 | $this->output->writeln( 192 | 'Loading providers from '.$this->http->getBaseUri().'' 193 | ); 194 | 195 | $this->providers = $this->provider->addFullPath( 196 | $this->package->getMainJson() 197 | ); 198 | 199 | if ($this->isEqual()) { 200 | return $this; 201 | } 202 | 203 | $this->providerIncludes = $this->provider->normalize($this->providers); 204 | $generator = $this->provider->getGenerator($this->providerIncludes); 205 | 206 | $this->progressBar->start(count($this->providerIncludes)); 207 | 208 | $success = function ($body, $path) { 209 | $this->provider->setDownloaded($path); 210 | $this->filesystem->write($path, $body); 211 | }; 212 | 213 | $this->http->pool($generator, $success, $this->getClosureComplete()); 214 | $this->progressBar->end(); 215 | $this->showErrors(); 216 | 217 | // If initialized can have provider downloaded by half 218 | if ($generator->getReturn() && !$this->initialized) { 219 | $this->output->writeln('All providers are updated'); 220 | 221 | return $this->setExitCode(0); 222 | } 223 | 224 | return $this; 225 | } 226 | 227 | /** 228 | * Show errors. 229 | * 230 | * @return Create 231 | */ 232 | protected function showErrors():Create 233 | { 234 | $errors = $this->http->getPoolErrors(); 235 | 236 | if (!$this->isVerbose() || empty($errors)) { 237 | return $this; 238 | } 239 | 240 | $rows = []; 241 | foreach ($errors as $path => $reason) { 242 | list('code' => $code, 'host' => $host, 'message' => $message) = $reason; 243 | 244 | $error = $code; 245 | if (!$error) { 246 | $error = $message; 247 | } 248 | 249 | $rows[] = [ 250 | ''.$host.'', 251 | ''.$this->shortname($path).'', 252 | ''.$error.'', 253 | ]; 254 | } 255 | 256 | $table = new Table($this->output); 257 | $table->setHeaders(['Mirror', 'Path', 'Error']); 258 | $table->setRows($rows); 259 | $table->render(); 260 | 261 | return $this; 262 | } 263 | 264 | /** 265 | * Disable mirror when due lots of errors. 266 | */ 267 | protected function disableDueErrors() 268 | { 269 | $mirrors = $this->http->getMirror()->toArray(); 270 | 271 | foreach ($mirrors as $mirror) { 272 | $total = $this->http->getTotalErrorByMirror($mirror); 273 | if ($total < 1000) { 274 | continue; 275 | } 276 | 277 | $this->output->write(PHP_EOL); 278 | $this->output->writeln( 279 | 'Due to '.$total. 280 | ' errors mirror '. 281 | $mirror.' will be disabled' 282 | ); 283 | $this->output->write(PHP_EOL); 284 | $this->http->getMirror()->remove($mirror); 285 | } 286 | 287 | return $this; 288 | } 289 | 290 | /** 291 | * Download packages listed on provider-*.json on public/p dir. 292 | * 293 | * @return Create 294 | */ 295 | protected function downloadPackages():Create 296 | { 297 | $providerIncludes = $this->provider->getDownloaded(); 298 | $totalProviders = count($providerIncludes); 299 | 300 | foreach ($providerIncludes as $counter => $uri) { 301 | $this->currentProvider = $uri; 302 | $shortname = $this->shortname($uri); 303 | 304 | ++$counter; 305 | $this->output->writeln( 306 | '['.$counter.'/'.$totalProviders.']'. 307 | ' Loading packages from '.$shortname.' provider' 308 | ); 309 | 310 | if ($this->initialized) { 311 | $this->http->useMirrors(); 312 | } 313 | 314 | $this->providerPackages = $this->package->getProvider($uri); 315 | $generator = $this->package->getGenerator($this->providerPackages); 316 | $this->progressBar->start(count($this->providerPackages)); 317 | $this->poolPackages($generator); 318 | $this->progressBar->end(); 319 | $this->showErrors()->disableDueErrors()->fallback(); 320 | } 321 | 322 | return $this; 323 | } 324 | 325 | /** 326 | * @param Generator $generator 327 | * 328 | * @return Create 329 | */ 330 | protected function poolPackages(Generator $generator):Create 331 | { 332 | $this->http->pool( 333 | $generator, 334 | // Success 335 | function ($body, $path) { 336 | $this->filesystem->write($path, $body); 337 | $this->package->setDownloaded($path); 338 | }, 339 | // If complete, even failed and success 340 | $this->getClosureComplete() 341 | ); 342 | 343 | return $this; 344 | } 345 | 346 | /** 347 | * @return Closure 348 | */ 349 | protected function getClosureComplete():Closure 350 | { 351 | return function () { 352 | $this->progressBar->progress(); 353 | }; 354 | } 355 | 356 | /** 357 | * Fallback to main mirror when other mirrors failed. 358 | * 359 | * @return Create 360 | */ 361 | protected function fallback():Create 362 | { 363 | $total = count($this->http->getPoolErrors()); 364 | 365 | if (!$total) { 366 | return $this; 367 | } 368 | 369 | $shortname = $this->shortname($this->currentProvider); 370 | 371 | $this->output->writeln( 372 | 'Fallback packages from '.$shortname. 373 | ' provider to main mirror '.$this->http->getBaseUri().'' 374 | ); 375 | 376 | $this->providerPackages = $this->http->getPoolErrors(); 377 | $generator = $this->package->getGenerator($this->providerPackages); 378 | $this->progressBar->start($total); 379 | $this->poolPackages($generator); 380 | $this->progressBar->end(); 381 | $this->showErrors(); 382 | 383 | return $this; 384 | } 385 | 386 | /** 387 | * Generate HTML of index.html. 388 | */ 389 | protected function generateHtml():Create 390 | { 391 | ob_start(); 392 | $countryName = getenv('APP_COUNTRY_NAME'); 393 | $countryCode = getenv('APP_COUNTRY_CODE'); 394 | $maintainerMirror = getenv('MAINTAINER_MIRROR'); 395 | $maintainerProfile = getenv('MAINTAINER_PROFILE'); 396 | $maintainerRepo = getenv('MAINTAINER_REPO'); 397 | $maintainerLicense = getenv('MAINTAINER_LICENSE'); 398 | $tz = getenv('TZ'); 399 | $synced = getenv('SLEEP'); 400 | $file = $this->filesystem->getGzName('packages.json'); 401 | $exists = $this->filesystem->hasFile($file); 402 | $html = $this->filesystem->getFullPath('index.html'); 403 | 404 | $lastModified = false; 405 | if ($exists) { 406 | $lastModified = filemtime($html); 407 | unlink($html); 408 | } 409 | 410 | include_once getcwd().'/resources/index.html.php'; 411 | file_put_contents($html, ob_get_clean()); 412 | return $this; 413 | } 414 | } 415 | --------------------------------------------------------------------------------