Create::configure()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
nc 1
nop 0
dl 0
loc 8
ccs 0
cts 2
cp 0
crap 2
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Packagist Mirror.
7
 *
8
 * For the full license information, please view the LICENSE.md
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Webs\Mirror\Command;
13
14
use Symfony\Component\Console\Input\InputInterface;
15
use Symfony\Component\Console\Input\InputOption;
16
use Symfony\Component\Console\Output\OutputInterface;
17
use Symfony\Component\Console\Helper\Table;
18
use Webs\Mirror\Provider;
19
use stdClass;
20
use Generator;
21
use Closure;
22
23
/**
24
 * Create a mirror.
25
 *
26
 * @author Webysther Nunes <[email protected]>
27
 */
28
class Create extends Base
29
{
30
    /**
31
     * @var stdClass
32
     */
33
    protected $providers;
34
35
    /**
36
     * @var array
37
     */
38
    protected $providerIncludes;
39
40
    /**
41
     * @var string
42
     */
43
    protected $currentProvider;
44
45
    /**
46
     * @var array
47
     */
48
    protected $providerPackages;
49
50
    /**
51
     * @var Clean
52
     */
53
    protected $clean;
54
55
    /**
56
     * @var array
57 1
     */
58
    protected $latestErrorsShowed = [];
59 1
60 1
    /**
61 1
     * Errors before disable mirror
62
     */
63 1
    const ERROR_LIMIT = 100;
64
65
    /**
66
     * {@inheritdoc}
67
     */
68 1
    public function __construct($name = '')
69
    {
70 1
        parent::__construct('create');
71 1
        $this->setDescription(
72
            'Create/update packagist mirror'
73
        );
74 1
    }
75
76
    /**
77 1
     * {@inheritdoc}
78
     */
79
    protected function configure()
80
    {
81
        parent::configure();
82
        $this->addOption(
83
            'no-clean',
84
            null,
85
            InputOption::VALUE_NONE,
86
            "Don't search for deleted packages from metadata: php bin/mirror clean --help"
87
        );
88
    }
89
90
    /**
91
     * {@inheritdoc}
92
     */
93
    public function execute(InputInterface $input, OutputInterface $output):int
94
    {
95
        $this->initialize($input, $output);
96
        $this->bootstrap();
97 1
98
        // Download providers
99 1
        $this->downloadProviders();
100 1
101 1
        // Download packages
102 1
        if ($this->stop() || $this->downloadPackages()->stop()) {
103 1
            return $this->getExitCode();
104 1
        }
105 1
106 1
        // Move to new location
107
        $this->filesystem->move(self::DOT);
108
109
        // Clean
110
        if (!$this->input->getOption('no-clean')) {
111 1
            $this->setExitCode($this->clean->execute($input, $output));
112
        }
113 1
114
        if ($this->initialized) {
115 1
            $this->filesystem->delete(self::INIT);
116
        }
117
118
        return $this->getExitCode();
119
    }
120
121
    /**
122
     * @return void
123
     */
124
    public function bootstrap():void
125
    {
126
        $this->progressBar->setConsole($this->input, $this->output);
127
        $this->package->setConsole($this->input, $this->output);
128
        $this->package->setHttp($this->http);
129
        $this->package->setFilesystem($this->filesystem);
130
        $this->provider->setConsole($this->input, $this->output);
131
        $this->provider->setHttp($this->http);
132
        $this->provider->setFilesystem($this->filesystem);
133 1
    }
134
135
    /**
136 1
     * @param Clean $clean
137 1
     */
138
    public function setClean(Clean $clean):Create
139
    {
140 1
        $this->clean = $clean;
141
142 1
        return $this;
143
    }
144
145 1
    /**
146
     * @return int
147
     */
148
    protected function getExitCode():int
149
    {
150
        $this->generateHtml();
151
152
        return parent::getExitCode();
153
    }
154
155
    /**
156
     * Check if packages.json was changed.
157 1
     *
158 1
     * @return bool
159
     */
160
    protected function isEqual():bool
161 1
    {
162 1
        // if 'p/...' folder not found
163
        if (!is_dir($this->filesystem->getFullPath(self::TO))) {
164 1
            $this->filesystem->touch(self::INIT);
165
            $this->moveToPublic();
166
        }
167
168
        //usefull on dev frontend (move the frontend files every time):
169
        //$this->moveToPublic();
170
        $this->initialized = $this->filesystem->hasFile(self::INIT);
171
172 1
        $newPackages = json_encode($this->providers, JSON_PRETTY_PRINT);
173
174 1
        // No provider changed? Just relax...
175 1
        if ($this->filesystem->has(self::MAIN) && !$this->initialized) {
176
            $old = $this->filesystem->getHashFile(self::MAIN);
177
            $new = $this->filesystem->getHash($newPackages);
178 1
179 1
            if ($old == $new) {
180
                $this->output->writeln(self::MAIN.' <info>updated</>');
181
                $this->setExitCode(0);
182 1
183
                return true;
184
            }
185
        }
186 1
187 1
        if (!$this->filesystem->has(self::MAIN)) {
188
            $this->initialized = true;
189 1
        }
190
191 1
        $this->provider->setInitialized($this->initialized);
192 1
        $this->filesystem->write(self::DOT, $newPackages);
193 1
194 1
        return false;
195
    }
196 1
197 1
    /**
198 1
     * Copy all public resources to public
199
     *
200
     * @return void
201 1
     */
202
    protected function moveToPublic():void
203
    {
204
        $from = getcwd().'/resources/public/';
205
        foreach (new \DirectoryIterator($from) as $fileInfo) {
206
            if ($fileInfo->isDot()) {
207 1
                continue;
208
            }
209
            $file = $fileInfo->getFilename();
210
            $to = $this->filesystem->getFullPath($file);
211
            copy($from.$file, $to);
212
        }
213
    }
214
215 1
    /**
216
     * Download packages.json & provider-xxx$xxx.json.
217 1
     *
218
     * @return Create
219 1
     */
220 1
    protected function downloadProviders():Create
221
    {
222
        $uri = $this->http->getBaseUri();
223
224
        $this->output->writeln(
225
            'Loading providers from <info>'.$uri.'</>'
226
        );
227
228
        if (!filter_var($uri, FILTER_VALIDATE_URL)) {
229
            $this->output->writeln('<error>The main mirror url is invalid!</>');
230
            return $this->setExitCode(1);
231
        }
232
233
        $this->providers = $this->provider->getPackagesJson(
234
            $this->package->getMainJson()
235
        );
236
237
        if ($this->isEqual()) {
238
            return $this;
239
        }
240
241
        $this->providerIncludes = $this->provider->normalize($this->providers);
242
        $generator = $this->provider->getGenerator($this->providerIncludes);
243
244
        $this->progressBar->start(count($this->providerIncludes));
245
246
        $success = function($body, $path) {
247
            $this->provider->setDownloaded($path);
248
            $this->filesystem->write($path, $body);
249
        };
250
251
        $this->http->pool($generator, $success, $this->getClosureComplete());
252
        $this->progressBar->end();
253
        if (!$this->progressBar->isDisabled()) {
254
            $this->output->write(PHP_EOL);
255
        }
256
        $this->showErrors();
257
258
        // If initialized can have provider downloaded by half
259
        if ($generator->getReturn() && !$this->initialized) {
260
            $this->output->writeln('All providers are <info>updated</>');
261
262
            return $this->setExitCode(0);
263
        }
264
265
        return $this;
266
    }
267
268
    /**
269
     * Show errors.
270
     *
271
     * @return Create
272
     */
273
    protected function showErrors():Create
274
    {
275
        if (!$this->isDebug()) {
276
            return $this;
277
        }
278 1
279
        $errors = $this->http->getPoolErrors();
280 1
        $rows = [];
281 1
        foreach ($errors as $path => $reason) {
282
            list('code' => $code, 'host' => $host, 'message' => $message) = $reason;
283 1
284 1
            $error = $code;
285 1
            if (!$error) {
286
                $error = $message;
287 1
            }
288 1
289 1
            $rows[] = [
290 1
                '<info>'.$host.'</>',
291
                '<comment>'.$this->shortname($path).'</>',
292
                '<error>'.$error.'</>',
293 1
            ];
294 1
        }
295
296
        if (!count($rows)) {
297 1
            return $this;
298 1
        }
299 1
300 1
        $table = new Table($this->output);
301
        $table->setHeaders(['Mirror', 'Path', 'Error']);
302
        $table->setRows($rows);
303
        $table->render();
304
305
        return $this;
306
    }
307
308
    /**
309
     * Disable mirror when due lots of errors.
310
     */
311
    protected function disableDueErrors()
312
    {
313 1
        $mirrors = $this->http->getMirror()->toArray();
314
315 1
        foreach ($mirrors as $mirror) {
316 1
            if (empty($mirror)) {
317
                continue;
318 1
            }
319 1
            
320 1
            $uri = parse_url($mirror);
321 1
            $base = $uri['scheme'].'://'.$uri['host'];
322
            $total = $this->http->getTotalErrorByMirror($base);
323 1
324
            if (!isset($this->latestErrorsShowed[$base])) {
325
                $this->latestErrorsShowed[$base] = 0;
326
            }
327
328
            # Errors before disable mirror
329
            if ($total < self::ERROR_LIMIT) {
330
                //print_r($this->isVeryVerbose());
331
                if ($this->isVeryVerbose() && $total > 1) {
332
                    //print_r($this->latestErrorsShowed);
333
                    //print($base.PHP_EOL);
334 1
                    if ($this->latestErrorsShowed[$base] == $total) {
335 1
                        continue;
336 1
                    }
337
338
                    $this->latestErrorsShowed[$base] = $total;
339
                    $softError = '<error>'.$total.' errors</> mirror <comment>';
340
                    $softError = $softError.$mirror.'</>';
341
                    $this->output->writeln($softError);
342
                }
343
                
344
                continue;
345
            }
346
347
            $this->output->write(PHP_EOL);
348
            $this->output->writeln(
349
                'Due to <error>'.$total.
350
                ' errors</> mirror <comment>'.
351
                $mirror.'</> will be disabled'
352
            );
353
            $this->output->write(PHP_EOL);
354
            $this->http->getMirror()->remove($mirror);
355
        }
356
357
        return $this;
358
    }
359
360
    /**
361
     * Download packages listed on provider-*.json on /p dir.
362
     *
363
     * @return Create
364
     */
365
    protected function downloadPackages():Create
366
    {
367
        $providerIncludes = $this->provider->getDownloaded();
368
        $totalProviders = count($providerIncludes);
369
370
        foreach ($providerIncludes as $counter => $uri) {
371
            $this->currentProvider = $uri;
372
            $shortname = $this->shortname($uri);
373
374
            ++$counter;
375
            $this->output->writeln(
376
                '['.$counter.'/'.$totalProviders.']'.
377
                ' Loading packages from <info>'.$shortname.'</> provider'
378
            );
379
380
            if ($this->initialized) {
381
                $this->http->useMirrors();
382
            }
383
384
            $this->providerPackages = $this->package->getProvider($uri);
385
            $generator = $this->package->getGenerator($this->providerPackages);
386
            $this->progressBar->start(count($this->providerPackages));
387
            $this->poolPackages($generator);
388
            $this->progressBar->end();
389
            if (!$this->progressBar->isDisabled()) {
390
                $this->output->write(PHP_EOL);
391
            }
392
            $this->showErrors()->disableDueErrors()->fallback();
393
        }
394
395
        return $this;
396
    }
397
398
    /**
399
     * @param Generator $generator
400
     *
401
     * @return Create
402
     */
403
    protected function poolPackages(Generator $generator):Create
404
    {
405
        $this->http->pool(
406
            $generator,
407
            // Success
408
            function($body, $path) {
409
                $this->filesystem->write($path, $body);
410
                $this->package->setDownloaded($path);
411
            },
412
            // If complete, even failed and success
413
            $this->getClosureComplete()
414
        );
415
416
        return $this;
417
    }
418
419
    /**
420
     * @return Closure
421
     */
422
    protected function getClosureComplete():Closure
423
    {
424
        return function() {
425
            $this->progressBar->progress();
426
        };
427
    }
428
429
    /**
430
     * Fallback to main mirror when other mirrors failed.
431
     *
432
     * @return Create
433
     */
434
    protected function fallback():Create
435
    {
436
        $total = count($this->http->getPoolErrors());
437
438
        if (!$total) {
439
            return $this;
440
        }
441
442
        $shortname = $this->shortname($this->currentProvider);
443
444
        $this->output->writeln(
445
            'Fallback packages from <info>'.$shortname.
446
            '</> provider to main mirror <info>'.$this->http->getBaseUri().'</>'
447
        );
448
449
        $this->providerPackages = $this->http->getPoolErrors();
450
        $generator = $this->package->getGenerator($this->providerPackages);
451
        $this->progressBar->start($total);
452
        $this->poolPackages($generator);
453
        $this->progressBar->end();
454
        if (!$this->progressBar->isDisabled()) {
455
            $this->output->write(PHP_EOL);
456
        }
457
        $this->showErrors();
458
459
        return $this;
460
    }
461
462
    /**
463
     * Generate HTML of index.html.
464
     */
465
    protected function generateHtml():Create
466
    {
467
        ob_start();
468
        $countryName = getenv('APP_COUNTRY_NAME');
469
        $countryCode = getenv('APP_COUNTRY_CODE');
470
        $maintainerMirror = getenv('MAINTAINER_MIRROR');
471
        $maintainerProfile = getenv('MAINTAINER_PROFILE');
472
        $maintainerRepo = getenv('MAINTAINER_REPO');
473
        $maintainerLicense = getenv('MAINTAINER_LICENSE');
474
        $since = getenv('SINCE');
0 ignored issues
show
Unused Code introduced by
The assignment to $since is dead and can be removed.
Loading history...
475
        $tz = getenv('TZ');
0 ignored issues
show
Unused Code introduced by
The assignment to $tz is dead and can be removed.
Loading history...
476
        $synced = getenv('SLEEP');
477
        $googleAnalyticsId = getenv('GOOGLE_ANALYTICS_ID');
478
        $googleAnalyticsMainId = getenv('GOOGLE_ANALYTICS_MAIN_ID');
479
        $file = $this->filesystem->getGzName('packages.json');
480
        $exists = $this->filesystem->hasFile('index.html');
481
        $html = $this->filesystem->getFullPath('index.html');
482
483
        $lastModified = false;
484
        if ($exists) {
485
            $lastModified = filemtime($html);
486
            unlink($html);
487
        }
488
489
        include_once getcwd().'/resources/index.html.php';
490
        file_put_contents($html, ob_get_clean());
491
        return $this;
492
    }
493
}
494