Completed
Push — feature/decoupled ( 010006...b00aba )
by Webysther
02:04
created

Create::loadPackagesJson()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 11
ccs 0
cts 9
cp 0
crap 2
rs 9.4285
c 0
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\Output\OutputInterface;
16
use Webs\Mirror\ShortName;
17
use Webs\Mirror\Provider;
18
use stdClass;
19
use Generator;
20
use Closure;
21
22
/**
23
 * Create a mirror.
24
 *
25
 * @author Webysther Nunes <[email protected]>
26
 */
27
class Create extends Base
28
{
29
    use ShortName;
30
31
    /**
32
     * @var stdClass
33
     */
34
    protected $providers;
35
36
    /**
37
     * @var array
38
     */
39
    protected $providerIncludes;
40
41
    /**
42
     * @var string
43
     */
44
    protected $currentProvider;
45
46
    /**
47
     * @var array
48
     */
49
    protected $providerPackages;
50
51
    /**
52
     * @var Clean
53
     */
54
    protected $clean;
55
56
    /**
57
     * {@inheritdoc}
58
     */
59
    public function __construct($name = '')
60
    {
61
        parent::__construct('create');
62
        $this->setDescription(
63
            'Create/update packagist mirror'
64
        );
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    public function execute(InputInterface $input, OutputInterface $output):int
71
    {
72
        $this->progressBar->setConsole($input, $output);
73
        $this->package->setConsole($input, $output);
74
        $this->package->setHttp($this->http);
75
        $this->provider->setConsole($input, $output);
76
        $this->provider->setHttp($this->http);
77
78
        // Download providers, with repository, is incremental
79
        if ($this->downloadProviders()->stop()) {
80
            return $this->getExitCode();
81
        }
82
83
        // Download packages
84
        if ($this->downloadPackages()->stop()) {
85
            return $this->getExitCode();
86
        }
87
88
        // Switch .packagist.json to packagist.json
89
        if ($this->switch()->stop()) {
90
            return $this->getExitCode();
91
        }
92
93
        $this->setExitCode($this->clean->execute($input, $output));
94
95
        if ($this->initialized) {
96
            $this->filesystem->delete(self::INIT);
97
        }
98
99
        return $this->getExitCode();
100
    }
101
102
    /**
103
     * @param Clean $clean
104
     */
105
    public function setClean(Clean $clean):Create
106
    {
107
        $this->clean = $clean;
108
109
        return $this;
110
    }
111
112
    /**
113
     * @return int
114
     */
115
    protected function getExitCode():int
116
    {
117
        $this->generateHtml();
118
119
        return $this->exitCode;
120
    }
121
122
    /**
123
     * Check if packages.json was changed.
124
     *
125
     * @return bool
126
     */
127
    protected function isEqual():bool
128
    {
129
        // if 'p/...' folder not found
130
        if (!$this->filesystem->has(self::TO)) {
131
            $this->filesystem->touch(self::INIT);
132
        }
133
134
        $this->initialized = $this->filesystem->has(self::INIT);
135
136
        $newPackages = json_encode($this->providers, JSON_PRETTY_PRINT);
137
138
        // No provider changed? Just relax...
139
        if ($this->canSkip(self::MAIN)) {
140
            $old = $this->filesystem->getHashFile(self::MAIN);
141
            $new = $this->filesystem->getHash($newPackages);
142
143
            if ($old == $new) {
144
                $this->output->writeln(self::MAIN.' <info>updated</>');
145
                $this->setExitCode(0);
146
147
                return true;
148
            }
149
        }
150
151
        $this->filesystem->write(self::DOT, $newPackages);
152
153
        return false;
154
    }
155
156
    /**
157
     * Switch current packagist.json to space and .packagist to packagist.json.
158
     *
159
     * @return Create
160
     */
161
    protected function switch ():Create
162
    {
163
        // If .packages.json dont exists
164
        if (!$this->filesystem->has(self::DOT)) {
165
            return $this;
166
        }
167
168
        // Move to new location
169
        $this->filesystem->move(self::DOT, self::MAIN);
170
171
        return $this;
172
    }
173
174
    /**
175
     * Download packages.json & provider-xxx$xxx.json.
176
     *
177
     * @return Create
178
     */
179
    protected function downloadProviders():Create
180
    {
181
        $this->output->writeln(
182
            'Loading providers from <info>'.$this->http->getBaseUri().'</>'
183
        );
184
185
        $this->providers = $this->provider->addFullPath(
186
            $this->package->loadMainJson()
187
        );
188
189
190
        if ($this->isEqual()) {
191
            return $this;
192
        }
193
194
        $this->providerIncludes = $this->provider->normalize($this->providers);
195
        $generator = $this->getProvidersGenerator();
196
197
        $this->progressBar->start(count($this->providerIncludes));
198
199
        $success = function($body, $path) {
200
            $this->filesystem->write($path, $body);
201
        };
202
203
        $this->http->pool($generator, $success, $this->getClosureComplete());
204
        $this->progressBar->end();
205
        $this->showErrors();
206
207
        if ($generator->getReturn()) {
208
            $this->output->writeln('All providers are <info>updated</>');
209
210
            return $this->setExitCode(0);
211
        }
212
213
        return $this;
214
    }
215
216
    /**
217
     * @return Closure
218
     */
219
    protected function getClosureComplete():Closure
220
    {
221
        return function() {
222
            $this->progressBar->progress();
223
        };
224
    }
225
226
    /**
227
     * Download packages.json & provider-xxx$xxx.json.
228
     *
229
     * @return Generator Providers downloaded
230
     */
231
    protected function getProvidersGenerator():Generator
232
    {
233
        $providerIncludes = array_keys($this->providerIncludes);
234
        $updated = true;
235
        foreach ($providerIncludes as $uri) {
236
            if ($this->canSkip($uri)) {
237
                continue;
238
            }
239
240
            $updated = false;
241
            yield $uri => $this->http->getRequest($uri);
242
        }
243
244
        return $updated;
245
    }
246
247
    /**
248
     * @param string $path
249
     *
250
     * @return bool
251
     */
252
    protected function canSkip(string $path):bool
253
    {
254
        if ($this->filesystem->has($path) && !$this->initialized) {
255
            return true;
256
        }
257
258
        return false;
259
    }
260
261
    /**
262
     * Show errors.
263
     *
264
     * @return Create
265
     */
266
    protected function showErrors():Create
267
    {
268
        if (!$this->isVerbose()) {
269
            return $this;
270
        }
271
272
        $errors = $this->http->getPoolErrors();
273
        if (count($errors) === 0) {
274
            return $this;
275
        }
276
277
        foreach ($errors as $path => $reason) {
278
            $shortname = $this->shortname($path);
279
            $error = $reason->getCode();
280
            $host = $reason->getRequest()->getUri()->getHost();
281
282
            $this->output->write(
283
                "<comment>$shortname</> failed from ".
284
                "<info>$host</> with HTTP error"
285
            );
286
287
            if (!$error) {
288
                $this->output->writeln(
289
                    ':'.PHP_EOL.'<error>'.$reason->getMessage().'</>'
290
                );
291
                continue;
292
            }
293
294
            $this->output->writeln(" <error>$error</>");
295
        }
296
297
        $this->output->write(PHP_EOL);
298
299
        return $this;
300
    }
301
302
    /**
303
     * Disable mirror when due lots of errors.
304
     */
305
    protected function disableDueErrors()
306
    {
307
        $errors = $this->http->getPoolErrors();
308
        if (count($errors) === 0) {
309
            return $this;
310
        }
311
312
        $counter = [];
313
314
        foreach ($errors as $reason) {
315
            $uri = $reason->getRequest()->getUri();
316
            $host = $uri->getScheme().'://'.$uri->getHost();
317
318
            if (!isset($counter[$host])) {
319
                $counter[$host] = 0;
320
            }
321
322
            ++$counter[$host];
323
        }
324
325
        $mirrors = $this->http->getMirror()->toArray();
326
327
        foreach ($mirrors as $mirror) {
328
            $total = $counter[$mirror];
329
            if ($total < 1000) {
330
                continue;
331
            }
332
333
            $this->output->write(PHP_EOL);
334
            $this->output->writeln(
335
                '<error>Due to '.
336
                $total.' errors mirror '.
337
                $mirror.' will be disabled</>'
338
            );
339
            $this->output->write(PHP_EOL);
340
            $this->http->getMirror()->remove($mirror);
341
        }
342
343
        return $this;
344
    }
345
346
    /**
347
     * @param string $uri
348
     *
349
     * @return Create
350
     */
351
    protected function loadProviderPackages(string $uri):Create
352
    {
353
        $providers = json_decode($this->filesystem->read($uri))->providers;
354
        $this->providerPackages = $this->package->normalize($providers);
355
        $this->currentProvider = $uri;
356
357
        return $this;
358
    }
359
360
    /**
361
     * Download packages listed on provider-*.json on public/p dir.
362
     *
363
     * @return Create
364
     */
365
    protected function downloadPackages():Create
366
    {
367
        $totalProviders = count($this->providerIncludes);
368
        $currentProvider = 0;
369
370
        $providerIncludes = array_keys($this->providerIncludes);
371
        foreach ($providerIncludes as $uri) {
372
            $shortname = $this->shortname($uri);
373
374
            $this->output->writeln(
375
                '['.++$currentProvider.'/'.$totalProviders.']'.
376
                ' Loading packages from <info>'.$shortname.'</> provider'
377
            );
378
379
            $this->http->useMirrors();
380
            $generator = $this->loadProviderPackages($uri)->getPackagesGenerator();
381
            if (empty(iterator_to_array($generator))) {
382
                continue;
383
            }
384
385
            $this->progressBar->start(count($this->providerPackages));
386
            $this->poolPackages($generator);
387
            $this->progressBar->end();
388
            $this->showErrors()->disableDueErrors()->fallback();
389
        }
390
391
        return $this;
392
    }
393
394
    /**
395
     * Download only a package.
396
     *
397
     * @return Generator Providers downloaded
398
     */
399
    protected function getPackagesGenerator():Generator
400
    {
401
        $providerPackages = array_keys($this->providerPackages);
402
        foreach ($providerPackages as $uri) {
403
            if ($this->filesystem->has($uri)) {
404
                $this->progressBar->progress();
405
                continue;
406
            }
407
408
            if ($this->initialized) {
409
                $uri = $this->http->getMirror()->getNext().'/'.$uri;
410
            }
411
412
            yield $uri => $this->http->getRequest($uri);
413
        }
414
    }
415
416
    /**
417
     * @param Generator $generator
418
     * @param bool|bool $useMirrors
419
     *
420
     * @return Create
421
     */
422
    protected function poolPackages(Generator $generator):Create
423
    {
424
        $this->http->pool(
425
            $generator,
426
            // Success
427
            function($body, $path) {
428
                $this->filesystem->write($path, $body);
429
                $this->package->setDownloaded($path);
430
            },
431
            // If complete, even failed and success
432
            $this->getClosureComplete()
433
        );
434
435
        return $this;
436
    }
437
438
    protected function fallback():Create
439
    {
440
        $total = count($this->http->getPoolErrors());
441
442
        if (!$total) {
443
            return $this;
444
        }
445
446
        $shortname = $this->shortname($this->currentProvider);
447
448
        $this->output->writeln(
449
            'Fallback packages from <info>'.$shortname.
450
            '</> provider to main mirror <info>'.$this->http->getBaseUri().'</>'
451
        );
452
453
        $this->providerPackages = $this->http->getPoolErrors();
454
        $generator = $this->getPackagesGenerator();
455
        $this->progressBar->start($total);
456
        $this->poolPackages($generator);
457
        $this->progressBar->end();
458
        $this->showErrors();
459
460
        return $this;
461
    }
462
463
    /**
464
     * Generate HTML of index.html.
465
     */
466
    protected function generateHtml():Create
467
    {
468
        ob_start();
469
        include getcwd().'/resources/index.html.php';
470
        $this->filesystem->write('index.html', ob_get_clean());
471
472
        return $this;
473
    }
474
}
475